mirror of
https://github.com/sle118/squeezelite-esp32.git
synced 2025-12-09 13:07:03 +03:00
move to new cspot
This commit is contained in:
53
components/spotify/cspot/src/AccessKeyFetcher.cpp
Normal file
53
components/spotify/cspot/src/AccessKeyFetcher.cpp
Normal file
@@ -0,0 +1,53 @@
|
||||
#include "AccessKeyFetcher.h"
|
||||
#include <cstring>
|
||||
#include "Logger.h"
|
||||
#include "Utils.h"
|
||||
|
||||
using namespace cspot;
|
||||
|
||||
AccessKeyFetcher::AccessKeyFetcher(std::shared_ptr<cspot::Context> ctx) {
|
||||
this->ctx = ctx;
|
||||
}
|
||||
|
||||
AccessKeyFetcher::~AccessKeyFetcher() {}
|
||||
|
||||
bool AccessKeyFetcher::isExpired() {
|
||||
if (accessKey.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ctx->timeProvider->getSyncedTimestamp() > expiresAt) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void AccessKeyFetcher::getAccessKey(AccessKeyFetcher::Callback callback) {
|
||||
if (!isExpired()) {
|
||||
return callback(accessKey);
|
||||
}
|
||||
|
||||
CSPOT_LOG(info, "Access token expired, fetching new one...");
|
||||
|
||||
std::string url =
|
||||
string_format("hm://keymaster/token/authenticated?client_id=%s&scope=%s",
|
||||
CLIENT_ID.c_str(), SCOPES.c_str());
|
||||
auto timeProvider = this->ctx->timeProvider;
|
||||
|
||||
ctx->session->execute(
|
||||
MercurySession::RequestType::GET, url,
|
||||
[this, timeProvider, callback](MercurySession::Response& res) {
|
||||
if (res.fail) return;
|
||||
char* accessKeyJson = (char*)res.parts[0].data();
|
||||
auto accessJSON = std::string(accessKeyJson, strrchr(accessKeyJson, '}') - accessKeyJson + 1);
|
||||
auto jsonBody = nlohmann::json::parse(accessJSON);
|
||||
this->accessKey = jsonBody["accessToken"];
|
||||
int expiresIn = jsonBody["expiresIn"];
|
||||
expiresIn = expiresIn / 2; // Refresh token before it expires
|
||||
|
||||
this->expiresAt =
|
||||
timeProvider->getSyncedTimestamp() + (expiresIn * 1000);
|
||||
callback(jsonBody["accessToken"]);
|
||||
});
|
||||
}
|
||||
@@ -1,113 +1,23 @@
|
||||
#include "ApResolve.h"
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <iostream>
|
||||
#include <ctype.h>
|
||||
#include <cstring>
|
||||
#include <stdlib.h>
|
||||
#include <sys/types.h>
|
||||
#ifdef _WIN32
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#include "win32shim.h"
|
||||
#else
|
||||
#include <sys/socket.h>
|
||||
#include <netdb.h>
|
||||
#include <netinet/in.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
#include <sstream>
|
||||
#include <fstream>
|
||||
#include "Logger.h"
|
||||
#include <cJSON.h>
|
||||
#include <ConfigJSON.h>
|
||||
#include <random>
|
||||
|
||||
ApResolve::ApResolve() {}
|
||||
using namespace cspot;
|
||||
|
||||
std::string ApResolve::getApList()
|
||||
{
|
||||
// hostname lookup
|
||||
struct hostent *host = gethostbyname("apresolve.spotify.com");
|
||||
struct sockaddr_in client;
|
||||
|
||||
if ((host == NULL) || (host->h_addr == NULL))
|
||||
{
|
||||
CSPOT_LOG(error, "apresolve: DNS lookup error");
|
||||
throw std::runtime_error("Resolve failed");
|
||||
}
|
||||
|
||||
// Prepare socket
|
||||
bzero(&client, sizeof(client));
|
||||
client.sin_family = AF_INET;
|
||||
client.sin_port = htons(80);
|
||||
memcpy(&client.sin_addr, host->h_addr, host->h_length);
|
||||
|
||||
int sockFd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
|
||||
// Connect to spotify's server
|
||||
if (connect(sockFd, (struct sockaddr *)&client, sizeof(client)) < 0)
|
||||
{
|
||||
close(sockFd);
|
||||
CSPOT_LOG(error, "Could not connect to apresolve");
|
||||
throw std::runtime_error("Resolve failed");
|
||||
}
|
||||
|
||||
// Prepare HTTP get header
|
||||
std::stringstream ss;
|
||||
ss << "GET / HTTP/1.1\r\n"
|
||||
<< "Host: apresolve.spotify.com\r\n"
|
||||
<< "Accept: application/json\r\n"
|
||||
<< "Connection: close\r\n"
|
||||
<< "\r\n\r\n";
|
||||
|
||||
std::string request = ss.str();
|
||||
|
||||
// Send the request
|
||||
if (send(sockFd, request.c_str(), request.length(), 0) != (int)request.length())
|
||||
{
|
||||
CSPOT_LOG(error, "apresolve: can't send request");
|
||||
throw std::runtime_error("Resolve failed");
|
||||
}
|
||||
|
||||
char cur;
|
||||
|
||||
// skip read till json data
|
||||
while (read(sockFd, &cur, 1) > 0 && cur != '{');
|
||||
|
||||
auto jsonData = std::string("{");
|
||||
|
||||
// Read json structure
|
||||
while (read(sockFd, &cur, 1) > 0)
|
||||
{
|
||||
jsonData += cur;
|
||||
}
|
||||
|
||||
close(sockFd);
|
||||
|
||||
return jsonData;
|
||||
ApResolve::ApResolve(std::string apOverride)
|
||||
{
|
||||
this->apOverride = apOverride;
|
||||
}
|
||||
|
||||
std::string ApResolve::fetchFirstApAddress()
|
||||
{
|
||||
if (configMan->apOverride != "")
|
||||
if (apOverride != "")
|
||||
{
|
||||
return configMan->apOverride;
|
||||
return apOverride;
|
||||
}
|
||||
|
||||
// Fetch json body
|
||||
auto jsonData = getApList();
|
||||
auto request = bell::HTTPClient::get("https://apresolve.spotify.com/");
|
||||
std::string_view responseStr = request->body();
|
||||
|
||||
// Use cJSON to get first ap address
|
||||
auto root = cJSON_Parse(jsonData.c_str());
|
||||
auto apList = cJSON_GetObjectItemCaseSensitive(root, "ap_list");
|
||||
|
||||
auto firstAp = cJSON_GetArrayItem(apList, 0);
|
||||
auto data = std::string(firstAp->valuestring);
|
||||
|
||||
// release cjson memory
|
||||
cJSON_Delete(root);
|
||||
|
||||
return data;
|
||||
// parse json with nlohmann
|
||||
auto json = nlohmann::json::parse(responseStr);
|
||||
return json["ap_list"][0];
|
||||
}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
#include "AudioChunk.h"
|
||||
|
||||
std::vector<uint8_t> audioAESIV({0x72, 0xe0, 0x67, 0xfb, 0xdd, 0xcb, 0xcf, 0x77, 0xeb, 0xe8, 0xbc, 0x64, 0x3f, 0x63, 0x0d, 0x93});
|
||||
|
||||
AudioChunk::AudioChunk(uint16_t seqId, std::vector<uint8_t> &audioKey, uint32_t startPosition, uint32_t predictedEndPosition)
|
||||
{
|
||||
this->crypto = std::make_unique<Crypto>();
|
||||
this->seqId = seqId;
|
||||
this->audioKey = audioKey;
|
||||
this->startPosition = startPosition;
|
||||
this->endPosition = predictedEndPosition;
|
||||
this->decryptedData = std::vector<uint8_t>();
|
||||
this->isHeaderFileSizeLoadedSemaphore = std::make_unique<WrappedSemaphore>(5);
|
||||
this->isLoadedSemaphore = std::make_unique<WrappedSemaphore>(5);
|
||||
}
|
||||
|
||||
AudioChunk::~AudioChunk()
|
||||
{
|
||||
}
|
||||
|
||||
void AudioChunk::appendData(const std::vector<uint8_t> &data)
|
||||
{
|
||||
//if (this == nullptr) return;
|
||||
this->decryptedData.insert(this->decryptedData.end(), data.begin(), data.end());
|
||||
}
|
||||
|
||||
void AudioChunk::readData(uint8_t *target, size_t offset, size_t nbytes) {
|
||||
auto readPos = offset + nbytes;
|
||||
auto modulo = (readPos % 16);
|
||||
auto ivReadPos = readPos;
|
||||
if (modulo != 0) {
|
||||
ivReadPos += (16 - modulo);
|
||||
}
|
||||
if (ivReadPos > decryptedCount) {
|
||||
// calculate the IV for right position
|
||||
auto calculatedIV = this->getIVSum((oldStartPos + decryptedCount) / 16);
|
||||
|
||||
crypto->aesCTRXcrypt(this->audioKey, calculatedIV, decryptedData.data() + decryptedCount, ivReadPos - decryptedCount);
|
||||
|
||||
decryptedCount = ivReadPos;
|
||||
}
|
||||
memcpy(target, this->decryptedData.data() + offset, nbytes);
|
||||
|
||||
}
|
||||
|
||||
void AudioChunk::finalize()
|
||||
{
|
||||
this->oldStartPos = this->startPosition;
|
||||
this->startPosition = this->endPosition - this->decryptedData.size();
|
||||
this->isLoaded = true;
|
||||
}
|
||||
|
||||
// Basically just big num addition
|
||||
std::vector<uint8_t> AudioChunk::getIVSum(uint32_t n)
|
||||
{
|
||||
return bigNumAdd(audioAESIV, n);
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
#include "AudioChunkManager.h"
|
||||
#include "BellUtils.h"
|
||||
#include "Logger.h"
|
||||
|
||||
AudioChunkManager::AudioChunkManager()
|
||||
: bell::Task("AudioChunkManager", 4 * 1024, 0, 1) {
|
||||
this->chunks = std::vector<std::shared_ptr<AudioChunk>>();
|
||||
startTask();
|
||||
}
|
||||
|
||||
std::shared_ptr<AudioChunk>
|
||||
AudioChunkManager::registerNewChunk(uint16_t seqId,
|
||||
std::vector<uint8_t> &audioKey,
|
||||
uint32_t startPos, uint32_t endPos) {
|
||||
std::scoped_lock lock(chunkMutex);
|
||||
auto chunk =
|
||||
std::make_shared<AudioChunk>(seqId, audioKey, startPos * 4, endPos * 4);
|
||||
this->chunks.push_back(chunk);
|
||||
CSPOT_LOG(debug, "Chunk requested %d", seqId);
|
||||
|
||||
return chunk;
|
||||
}
|
||||
void AudioChunkManager::handleChunkData(std::vector<uint8_t> &data,
|
||||
bool failed) {
|
||||
auto audioPair = std::pair(data, failed);
|
||||
audioChunkDataQueue.push(audioPair);
|
||||
}
|
||||
|
||||
void AudioChunkManager::failAllChunks() {
|
||||
std::scoped_lock lock(chunkMutex);
|
||||
// Enumerate all the chunks and mark em all failed
|
||||
for (auto const &chunk : this->chunks) {
|
||||
if (!chunk->isLoaded) {
|
||||
chunk->isLoaded = true;
|
||||
chunk->isFailed = true;
|
||||
chunk->isHeaderFileSizeLoadedSemaphore->give();
|
||||
chunk->isLoadedSemaphore->give();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AudioChunkManager::close() {
|
||||
this->isRunning = false;
|
||||
this->failAllChunks();
|
||||
this->audioChunkDataQueue.clear();
|
||||
std::scoped_lock lock(this->runningMutex);
|
||||
}
|
||||
|
||||
void AudioChunkManager::runTask() {
|
||||
std::scoped_lock lock(this->runningMutex);
|
||||
this->isRunning = true;
|
||||
std::pair<std::vector<uint8_t>, bool> audioPair;
|
||||
while (isRunning) {
|
||||
if (this->audioChunkDataQueue.wtpop(audioPair, 100)) {
|
||||
std::scoped_lock lock(this->chunkMutex);
|
||||
auto data = audioPair.first;
|
||||
auto failed = audioPair.second;
|
||||
uint16_t seqId = ntohs(extract<uint16_t>(data, 0));
|
||||
|
||||
// Erase all chunks that are not referenced elsewhere anymore
|
||||
chunks.erase(
|
||||
std::remove_if(chunks.begin(), chunks.end(),
|
||||
[](std::shared_ptr<AudioChunk>& chunk) {
|
||||
return chunk.use_count() == 1;
|
||||
}),
|
||||
chunks.end());
|
||||
|
||||
try {
|
||||
for (auto const &chunk : this->chunks) {
|
||||
// Found the right chunk
|
||||
if (chunk != nullptr && chunk->seqId == seqId) {
|
||||
if (failed) {
|
||||
chunk->isFailed = true;
|
||||
chunk->startPosition = 0;
|
||||
chunk->endPosition = 0;
|
||||
chunk->isHeaderFileSizeLoadedSemaphore->give();
|
||||
chunk->isLoadedSemaphore->give();
|
||||
break;
|
||||
}
|
||||
|
||||
switch (data.size()) {
|
||||
case DATA_SIZE_HEADER: {
|
||||
CSPOT_LOG(debug, "ID: %d: header finalize!", seqId);
|
||||
auto headerSize = ntohs(extract<uint16_t>(data, 2));
|
||||
// Got file size!
|
||||
chunk->headerFileSize =
|
||||
ntohl(extract<uint32_t>(data, 5)) * 4;
|
||||
chunk->isHeaderFileSizeLoadedSemaphore->give();
|
||||
break;
|
||||
}
|
||||
case DATA_SIZE_FOOTER:
|
||||
if (chunk->endPosition > chunk->headerFileSize) {
|
||||
chunk->endPosition = chunk->headerFileSize;
|
||||
}
|
||||
CSPOT_LOG(debug, "ID: %d: finalize chunk!",
|
||||
seqId);
|
||||
chunk->finalize();
|
||||
chunk->isLoadedSemaphore->give();
|
||||
break;
|
||||
|
||||
default:
|
||||
auto actualData = std::vector<uint8_t>(
|
||||
data.begin() + 2, data.end());
|
||||
chunk->appendData(actualData);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Playback finished
|
||||
}
|
||||
121
components/spotify/cspot/src/AuthChallenges.cpp
Normal file
121
components/spotify/cspot/src/AuthChallenges.cpp
Normal file
@@ -0,0 +1,121 @@
|
||||
#include "AuthChallenges.h"
|
||||
|
||||
using namespace cspot;
|
||||
using random_bytes_engine =
|
||||
std::independent_bits_engine<std::default_random_engine, CHAR_BIT, uint8_t>;
|
||||
|
||||
AuthChallenges::AuthChallenges() {
|
||||
this->crypto = std::make_unique<Crypto>();
|
||||
this->clientHello = {};
|
||||
this->apResponse = {};
|
||||
this->authRequest = {};
|
||||
this->clientResPlaintext = {};
|
||||
}
|
||||
|
||||
AuthChallenges::~AuthChallenges() {
|
||||
// Destruct the protobufs
|
||||
pb_release(ClientHello_fields, &clientHello);
|
||||
pb_release(APResponseMessage_fields, &apResponse);
|
||||
pb_release(ClientResponsePlaintext_fields, &clientResPlaintext);
|
||||
pb_release(ClientResponseEncrypted_fields, &authRequest);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> AuthChallenges::prepareAuthPacket(
|
||||
std::vector<uint8_t>& authData, int authType, const std::string& deviceId,
|
||||
const std::string& username) {
|
||||
// prepare authentication request proto
|
||||
pbPutString(username, authRequest.login_credentials.username);
|
||||
|
||||
std::copy(authData.begin(), authData.end(),
|
||||
authRequest.login_credentials.auth_data.bytes);
|
||||
authRequest.login_credentials.auth_data.size = authData.size();
|
||||
|
||||
authRequest.login_credentials.typ = (AuthenticationType)authType;
|
||||
authRequest.system_info.cpu_family = CpuFamily_CPU_UNKNOWN;
|
||||
authRequest.system_info.os = Os_OS_UNKNOWN;
|
||||
|
||||
auto infoStr = std::string("cspot-player");
|
||||
pbPutString(infoStr, authRequest.system_info.system_information_string);
|
||||
pbPutString(deviceId, authRequest.system_info.device_id);
|
||||
|
||||
auto versionStr = std::string("cspot-1.1");
|
||||
pbPutString(versionStr, authRequest.version_string);
|
||||
authRequest.has_version_string = true;
|
||||
|
||||
return pbEncode(ClientResponseEncrypted_fields, &authRequest);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> AuthChallenges::solveApHello(
|
||||
std::vector<uint8_t>& helloPacket, std::vector<uint8_t>& data) {
|
||||
// Decode the response
|
||||
auto skipSize = std::vector<uint8_t>(data.begin() + 4, data.end());
|
||||
|
||||
pb_release(APResponseMessage_fields, &apResponse);
|
||||
pbDecode(apResponse, APResponseMessage_fields, skipSize);
|
||||
|
||||
auto diffieKey = std::vector<uint8_t>(
|
||||
apResponse.challenge.login_crypto_challenge.diffie_hellman.gs,
|
||||
apResponse.challenge.login_crypto_challenge.diffie_hellman.gs + 96);
|
||||
|
||||
// Compute the diffie hellman shared key based on the response
|
||||
auto sharedKey = this->crypto->dhCalculateShared(diffieKey);
|
||||
|
||||
// Init client packet + Init server packets are required for the hmac challenge
|
||||
data.insert(data.begin(), helloPacket.begin(), helloPacket.end());
|
||||
|
||||
// Solve the hmac challenge
|
||||
auto resultData = std::vector<uint8_t>(0);
|
||||
for (int x = 1; x < 6; x++) {
|
||||
auto challengeVector = std::vector<uint8_t>(1);
|
||||
challengeVector[0] = x;
|
||||
|
||||
challengeVector.insert(challengeVector.begin(), data.begin(), data.end());
|
||||
auto digest = crypto->sha1HMAC(sharedKey, challengeVector);
|
||||
resultData.insert(resultData.end(), digest.begin(), digest.end());
|
||||
}
|
||||
|
||||
auto lastVec =
|
||||
std::vector<uint8_t>(resultData.begin(), resultData.begin() + 0x14);
|
||||
|
||||
// Digest generated!
|
||||
auto digest = crypto->sha1HMAC(lastVec, data);
|
||||
clientResPlaintext.login_crypto_response.has_diffie_hellman = true;
|
||||
|
||||
std::copy(digest.begin(), digest.end(),
|
||||
clientResPlaintext.login_crypto_response.diffie_hellman.hmac);
|
||||
|
||||
// Get send and receive keys
|
||||
this->shanSendKey = std::vector<uint8_t>(resultData.begin() + 0x14,
|
||||
resultData.begin() + 0x34);
|
||||
this->shanRecvKey = std::vector<uint8_t>(resultData.begin() + 0x34,
|
||||
resultData.begin() + 0x54);
|
||||
|
||||
return pbEncode(ClientResponsePlaintext_fields, &clientResPlaintext);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> AuthChallenges::prepareClientHello() {
|
||||
// Prepare protobuf message
|
||||
this->crypto->dhInit();
|
||||
|
||||
// Copy the public key into diffiehellman hello packet
|
||||
std::copy(this->crypto->publicKey.begin(), this->crypto->publicKey.end(),
|
||||
clientHello.login_crypto_hello.diffie_hellman.gc);
|
||||
|
||||
clientHello.login_crypto_hello.diffie_hellman.server_keys_known = 1;
|
||||
clientHello.build_info.product = Product_PRODUCT_CLIENT;
|
||||
clientHello.build_info.platform = Platform2_PLATFORM_LINUX_X86;
|
||||
clientHello.build_info.version = SPOTIFY_VERSION;
|
||||
clientHello.feature_set.autoupdate2 = true;
|
||||
clientHello.cryptosuites_supported[0] = Cryptosuite_CRYPTO_SUITE_SHANNON;
|
||||
clientHello.padding[0] = 0x1E;
|
||||
|
||||
clientHello.has_feature_set = true;
|
||||
clientHello.login_crypto_hello.has_diffie_hellman = true;
|
||||
clientHello.has_padding = true;
|
||||
clientHello.has_feature_set = true;
|
||||
|
||||
// Generate the random nonce
|
||||
auto nonce = crypto->generateVectorWithRandomData(16);
|
||||
std::copy(nonce.begin(), nonce.end(), clientHello.client_nonce);
|
||||
return pbEncode(ClientHello_fields, &clientHello);
|
||||
}
|
||||
182
components/spotify/cspot/src/CDNTrackStream.cpp
Normal file
182
components/spotify/cspot/src/CDNTrackStream.cpp
Normal file
@@ -0,0 +1,182 @@
|
||||
#include "CDNTrackStream.h"
|
||||
|
||||
using namespace cspot;
|
||||
|
||||
CDNTrackStream::CDNTrackStream(
|
||||
std::shared_ptr<cspot::AccessKeyFetcher> accessKeyFetcher) {
|
||||
this->accessKeyFetcher = accessKeyFetcher;
|
||||
this->status = Status::INITIALIZING;
|
||||
this->trackReady = std::make_unique<bell::WrappedSemaphore>(5);
|
||||
this->crypto = std::make_unique<Crypto>();
|
||||
}
|
||||
|
||||
CDNTrackStream::~CDNTrackStream() {
|
||||
}
|
||||
|
||||
void CDNTrackStream::fail() {
|
||||
this->status = Status::FAILED;
|
||||
this->trackReady->give();
|
||||
}
|
||||
|
||||
void CDNTrackStream::fetchFile(const std::vector<uint8_t>& trackId,
|
||||
const std::vector<uint8_t>& audioKey) {
|
||||
this->status = Status::HAS_DATA;
|
||||
this->trackId = trackId;
|
||||
this->audioKey = std::vector<uint8_t>(audioKey.begin() + 4, audioKey.end());
|
||||
|
||||
accessKeyFetcher->getAccessKey([this, trackId, audioKey](std::string key) {
|
||||
CSPOT_LOG(info, "Received access key, fetching CDN URL...");
|
||||
|
||||
std::string requestUrl = string_format(
|
||||
"https://api.spotify.com/v1/storage-resolve/files/audio/interactive/"
|
||||
"%s?alt=json&product=9",
|
||||
bytesToHexString(trackId).c_str());
|
||||
|
||||
auto req = bell::HTTPClient::get(
|
||||
requestUrl,
|
||||
{bell::HTTPClient::ValueHeader({"Authorization", "Bearer " + key})});
|
||||
|
||||
std::string_view result = req->body();
|
||||
|
||||
auto jsonResult = nlohmann::json::parse(result);
|
||||
std::string cdnUrl = jsonResult["cdnurl"][0];
|
||||
if (this->status != Status::FAILED) {
|
||||
|
||||
this->cdnUrl = cdnUrl;
|
||||
this->status = Status::HAS_URL;
|
||||
CSPOT_LOG(info, "Received CDN URL, %s", cdnUrl.c_str());
|
||||
|
||||
this->openStream();
|
||||
this->trackReady->give();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
size_t CDNTrackStream::getPosition() {
|
||||
return this->position;
|
||||
}
|
||||
|
||||
void CDNTrackStream::seek(size_t newPos) {
|
||||
this->enableRequestMargin = true;
|
||||
this->position = newPos;
|
||||
}
|
||||
|
||||
void CDNTrackStream::openStream() {
|
||||
CSPOT_LOG(info, "Opening HTTP stream to %s", this->cdnUrl.c_str());
|
||||
|
||||
// Open connection, read first 128 bytes
|
||||
this->httpConnection = bell::HTTPClient::get(
|
||||
this->cdnUrl,
|
||||
{bell::HTTPClient::RangeHeader::range(0, OPUS_HEADER_SIZE - 1)});
|
||||
|
||||
this->httpConnection->stream().read((char*)header.data(), OPUS_HEADER_SIZE);
|
||||
this->totalFileSize =
|
||||
this->httpConnection->totalLength() - SPOTIFY_OPUS_HEADER;
|
||||
|
||||
this->decrypt(header.data(), OPUS_HEADER_SIZE, 0);
|
||||
|
||||
// Location must be dividable by 16
|
||||
size_t footerStartLocation =
|
||||
(this->totalFileSize - OPUS_FOOTER_PREFFERED + SPOTIFY_OPUS_HEADER) -
|
||||
(this->totalFileSize - OPUS_FOOTER_PREFFERED + SPOTIFY_OPUS_HEADER) % 16;
|
||||
|
||||
this->footer = std::vector<uint8_t>(
|
||||
this->totalFileSize - footerStartLocation + SPOTIFY_OPUS_HEADER);
|
||||
this->httpConnection->get(
|
||||
cdnUrl, {bell::HTTPClient::RangeHeader::last(footer.size())});
|
||||
|
||||
this->httpConnection->stream().read((char*)footer.data(),
|
||||
this->footer.size());
|
||||
|
||||
this->decrypt(footer.data(), footer.size(), footerStartLocation);
|
||||
CSPOT_LOG(info, "Header and footer bytes received");
|
||||
this->position = 0;
|
||||
this->lastRequestPosition = 0;
|
||||
this->lastRequestCapacity = 0;
|
||||
this->isConnected = true;
|
||||
}
|
||||
|
||||
size_t CDNTrackStream::readBytes(uint8_t* dst, size_t bytes) {
|
||||
size_t offsetPosition = position + SPOTIFY_OPUS_HEADER;
|
||||
size_t actualFileSize = this->totalFileSize + SPOTIFY_OPUS_HEADER;
|
||||
|
||||
if (position + bytes >= this->totalFileSize) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// // Opus tries to read header, use prefetched data
|
||||
if (offsetPosition < OPUS_HEADER_SIZE &&
|
||||
bytes + offsetPosition <= OPUS_HEADER_SIZE) {
|
||||
memcpy(dst, this->header.data() + offsetPosition, bytes);
|
||||
position += bytes;
|
||||
return bytes;
|
||||
}
|
||||
|
||||
// // Opus tries to read footer, use prefetched data
|
||||
if (offsetPosition >= (actualFileSize - this->footer.size())) {
|
||||
size_t toReadBytes = bytes;
|
||||
|
||||
if ((position + bytes) > this->totalFileSize) {
|
||||
// Tries to read outside of bounds, truncate
|
||||
toReadBytes = this->totalFileSize - position;
|
||||
}
|
||||
|
||||
size_t footerOffset =
|
||||
offsetPosition - (actualFileSize - this->footer.size());
|
||||
memcpy(dst, this->footer.data() + footerOffset, toReadBytes);
|
||||
|
||||
position += toReadBytes;
|
||||
return toReadBytes;
|
||||
}
|
||||
|
||||
// Data not in the headers. Make sense of whats going on.
|
||||
// Position in bounds :)
|
||||
if (offsetPosition >= this->lastRequestPosition &&
|
||||
offsetPosition < this->lastRequestPosition + this->lastRequestCapacity) {
|
||||
size_t toRead = bytes;
|
||||
|
||||
if ((toRead + offsetPosition) >
|
||||
this->lastRequestPosition + lastRequestCapacity) {
|
||||
toRead = this->lastRequestPosition + lastRequestCapacity - offsetPosition;
|
||||
}
|
||||
|
||||
memcpy(dst, this->httpBuffer.data() + offsetPosition - lastRequestPosition,
|
||||
toRead);
|
||||
position += toRead;
|
||||
|
||||
return toRead;
|
||||
} else {
|
||||
size_t requestPosition = (offsetPosition) - ((offsetPosition) % 16);
|
||||
if (this->enableRequestMargin && requestPosition > SEEK_MARGIN_SIZE) {
|
||||
requestPosition = (offsetPosition - SEEK_MARGIN_SIZE) -
|
||||
((offsetPosition - SEEK_MARGIN_SIZE) % 16);
|
||||
this->enableRequestMargin = false;
|
||||
}
|
||||
|
||||
this->httpConnection->get(
|
||||
cdnUrl, {bell::HTTPClient::RangeHeader::range(
|
||||
requestPosition, requestPosition + HTTP_BUFFER_SIZE - 1)});
|
||||
this->lastRequestPosition = requestPosition;
|
||||
this->lastRequestCapacity = this->httpConnection->contentLength();
|
||||
|
||||
this->httpConnection->stream().read((char*)this->httpBuffer.data(),
|
||||
lastRequestCapacity);
|
||||
this->decrypt(this->httpBuffer.data(), lastRequestCapacity,
|
||||
|
||||
this->lastRequestPosition);
|
||||
|
||||
return readBytes(dst, bytes);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
size_t CDNTrackStream::getSize() {
|
||||
return this->totalFileSize;
|
||||
}
|
||||
|
||||
void CDNTrackStream::decrypt(uint8_t* dst, size_t nbytes, size_t pos) {
|
||||
auto calculatedIV = bigNumAdd(audioAESIV, pos / 16);
|
||||
|
||||
this->crypto->aesCTRXcrypt(this->audioKey, calculatedIV, dst, nbytes);
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
#include "ChunkedAudioStream.h"
|
||||
#include "Logger.h"
|
||||
#include "BellUtils.h"
|
||||
|
||||
static size_t vorbisReadCb(void *ptr, size_t size, size_t nmemb, ChunkedAudioStream *self)
|
||||
{
|
||||
size_t readSize = 0;
|
||||
while (readSize < nmemb * size && self->byteStream->position() < self->byteStream->size() && self->isRunning) {
|
||||
size_t bytes = self->byteStream->read((uint8_t *) ptr + readSize, (size * nmemb) - readSize);
|
||||
if (bytes <= 0) {
|
||||
CSPOT_LOG(info, "unexpected end/error of stream");
|
||||
return readSize;
|
||||
}
|
||||
readSize += bytes;
|
||||
}
|
||||
return readSize;
|
||||
}
|
||||
static int vorbisCloseCb(ChunkedAudioStream *self)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int vorbisSeekCb(ChunkedAudioStream *self, int64_t offset, int whence)
|
||||
{
|
||||
if (whence == 0)
|
||||
{
|
||||
offset += SPOTIFY_HEADER_SIZE;
|
||||
}
|
||||
|
||||
static constexpr std::array<Whence, 3> seekDirections{
|
||||
Whence::START, Whence::CURRENT, Whence::END};
|
||||
|
||||
self->seek(offset, seekDirections.at(static_cast<size_t>(whence)));
|
||||
return 0;
|
||||
}
|
||||
|
||||
static long vorbisTellCb(ChunkedAudioStream *self)
|
||||
{
|
||||
return static_cast<long>(self->byteStream->position());
|
||||
}
|
||||
|
||||
ChunkedAudioStream::~ChunkedAudioStream()
|
||||
{
|
||||
}
|
||||
|
||||
ChunkedAudioStream::ChunkedAudioStream(std::vector<uint8_t> fileId, std::vector<uint8_t> audioKey, uint32_t duration, std::shared_ptr<MercuryManager> manager, uint32_t startPositionMs, bool isPaused)
|
||||
{
|
||||
this->duration = duration;
|
||||
this->startPositionMs = startPositionMs;
|
||||
this->isPaused = isPaused;
|
||||
|
||||
// auto beginChunk = manager->fetchAudioChunk(fileId, audioKey, 0, 0x4000);
|
||||
// beginChunk->keepInMemory = true;
|
||||
// while(beginChunk->isHeaderFileSizeLoadedSemaphore->twait() != 0);
|
||||
// this->fileSize = beginChunk->headerFileSize;
|
||||
// chunks.push_back(beginChunk);
|
||||
//
|
||||
// // File size is required for this packet to be downloaded
|
||||
// this->fetchTraillingPacket();
|
||||
|
||||
this->byteStream = std::make_shared<ChunkedByteStream>(manager);
|
||||
this->byteStream->setFileInfo(fileId, audioKey);
|
||||
this->byteStream->fetchFileInformation();
|
||||
vorbisFile = { };
|
||||
vorbisCallbacks =
|
||||
{
|
||||
(decltype(ov_callbacks::read_func)) & vorbisReadCb,
|
||||
(decltype(ov_callbacks::seek_func)) & vorbisSeekCb,
|
||||
(decltype(ov_callbacks::close_func)) & vorbisCloseCb,
|
||||
(decltype(ov_callbacks::tell_func)) & vorbisTellCb,
|
||||
};
|
||||
}
|
||||
|
||||
void ChunkedAudioStream::seekMs(uint32_t positionMs)
|
||||
{
|
||||
byteStream->setEnableLoadAhead(false);
|
||||
this->seekMutex.lock();
|
||||
ov_time_seek(&vorbisFile, positionMs);
|
||||
this->seekMutex.unlock();
|
||||
byteStream->setEnableLoadAhead(true);
|
||||
|
||||
CSPOT_LOG(debug, "--- Finished seeking!");
|
||||
}
|
||||
|
||||
void ChunkedAudioStream::startPlaybackLoop(uint8_t *pcmOut, size_t pcmOut_len)
|
||||
{
|
||||
|
||||
isRunning = true;
|
||||
|
||||
byteStream->setEnableLoadAhead(false);
|
||||
int32_t r = ov_open_callbacks(this, &vorbisFile, NULL, 0, vorbisCallbacks);
|
||||
CSPOT_LOG(debug, "--- Loaded file");
|
||||
if (this->startPositionMs != 0)
|
||||
{
|
||||
ov_time_seek(&vorbisFile, startPositionMs);
|
||||
}
|
||||
|
||||
bool eof = false;
|
||||
byteStream->setEnableLoadAhead(true);
|
||||
|
||||
while (!eof && isRunning)
|
||||
{
|
||||
if (!isPaused)
|
||||
{
|
||||
|
||||
this->seekMutex.lock();
|
||||
long ret = ov_read(&vorbisFile, (char *)&pcmOut[0], pcmOut_len, ¤tSection);
|
||||
this->seekMutex.unlock();
|
||||
if (ret == 0)
|
||||
{
|
||||
CSPOT_LOG(info, "EOL");
|
||||
// and done :)
|
||||
eof = true;
|
||||
}
|
||||
else if (ret < 0)
|
||||
{
|
||||
CSPOT_LOG(error, "An error has occured in the stream");
|
||||
|
||||
// Error in the stream
|
||||
}
|
||||
else
|
||||
{
|
||||
// Write the actual data
|
||||
pcmCallback(pcmOut, ret);
|
||||
// audioSink->feedPCMFrames(data);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
BELL_SLEEP_MS(100);
|
||||
}
|
||||
}
|
||||
|
||||
ov_clear(&vorbisFile);
|
||||
vorbisCallbacks = {};
|
||||
CSPOT_LOG(debug, "Track finished");
|
||||
finished = true;
|
||||
|
||||
if (eof)
|
||||
{
|
||||
this->streamFinishedCallback();
|
||||
}
|
||||
}
|
||||
//
|
||||
//void ChunkedAudioStream::fetchTraillingPacket()
|
||||
//{
|
||||
// auto startPosition = (this->fileSize / 4) - 0x1000;
|
||||
//
|
||||
// // AES block size is 16, so the index must be divisible by it
|
||||
// while ((startPosition * 4) % 16 != 0)
|
||||
// startPosition++; // ik, ugly lol
|
||||
//
|
||||
// auto endChunk = manager->fetchAudioChunk(fileId, audioKey, startPosition, fileSize / 4);
|
||||
// endChunk->keepInMemory = true;
|
||||
//
|
||||
// chunks.push_back(endChunk);
|
||||
// while (endChunk->isLoadedSemaphore->twait() != 0);
|
||||
//}
|
||||
|
||||
void ChunkedAudioStream::seek(size_t dpos, Whence whence)
|
||||
{
|
||||
auto seekPos = 0;
|
||||
switch (whence)
|
||||
{
|
||||
case Whence::START:
|
||||
seekPos = dpos;
|
||||
break;
|
||||
case Whence::CURRENT:
|
||||
seekPos = byteStream->position() + dpos;
|
||||
break;
|
||||
case Whence::END:
|
||||
seekPos = byteStream->size() + dpos;
|
||||
break;
|
||||
}
|
||||
byteStream->seek(seekPos);
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
#include "ChunkedByteStream.h"
|
||||
|
||||
ChunkedByteStream::ChunkedByteStream(std::shared_ptr<MercuryManager> manager) {
|
||||
this->mercuryManager = manager;
|
||||
this->pos = 167; // spotify header size
|
||||
}
|
||||
|
||||
void ChunkedByteStream::setFileInfo(std::vector<uint8_t> &fileId, std::vector<uint8_t> &audioKey) {
|
||||
this->audioKey = audioKey;
|
||||
this->fileId = fileId;
|
||||
}
|
||||
|
||||
void ChunkedByteStream::setEnableLoadAhead(bool loadAhead) {
|
||||
this->loadAheadEnabled = loadAhead;
|
||||
}
|
||||
|
||||
void ChunkedByteStream::fetchFileInformation() {
|
||||
std::shared_ptr<AudioChunk> beginChunk = mercuryManager->fetchAudioChunk(fileId, audioKey, 0, 0x4000);
|
||||
beginChunk->keepInMemory = true;
|
||||
while (beginChunk->isHeaderFileSizeLoadedSemaphore->twait() != 0);
|
||||
this->fileSize = beginChunk->headerFileSize;
|
||||
chunks.push_back(beginChunk);
|
||||
|
||||
auto startPosition = (this->fileSize / 4) - 0x1000;
|
||||
|
||||
// AES block size is 16, so the index must be divisible by it
|
||||
while ((startPosition * 4) % 16 != 0)
|
||||
startPosition++; // ik, ugly lol
|
||||
|
||||
auto endChunk = mercuryManager->fetchAudioChunk(fileId, audioKey, startPosition, fileSize / 4);
|
||||
endChunk->keepInMemory = true;
|
||||
|
||||
chunks.push_back(endChunk);
|
||||
requestChunk(0);
|
||||
}
|
||||
|
||||
std::shared_ptr<AudioChunk> ChunkedByteStream::getChunkForPosition(size_t position) {
|
||||
// Find chunks that fit in requested position
|
||||
for (auto chunk: chunks) {
|
||||
if (chunk->startPosition <= position && chunk->endPosition > position) {
|
||||
return chunk;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::shared_ptr<AudioChunk> ChunkedByteStream::requestChunk(uint16_t position) {
|
||||
auto chunk = this->mercuryManager->fetchAudioChunk(this->fileId, this->audioKey, position);
|
||||
|
||||
// Store a reference internally
|
||||
chunks.push_back(chunk);
|
||||
return chunk;
|
||||
}
|
||||
|
||||
|
||||
size_t ChunkedByteStream::read(uint8_t *buf, size_t nbytes) {
|
||||
std::scoped_lock lock(this->readMutex);
|
||||
auto chunk = getChunkForPosition(pos);
|
||||
uint16_t chunkIndex = this->pos / AUDIO_CHUNK_SIZE;
|
||||
|
||||
if (loadAheadEnabled) {
|
||||
for (auto it = chunks.begin(); it != chunks.end();) {
|
||||
if (((*it)->endPosition<pos || (*it)->startPosition>(pos + 2 * AUDIO_CHUNK_SIZE)) && !(*it)->keepInMemory) {
|
||||
it = chunks.erase(it);
|
||||
} else {
|
||||
it++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Request chunk if does not exist
|
||||
if (chunk == nullptr) {
|
||||
BELL_LOG(info, "cspot", "Chunk not found, requesting %d", chunkIndex);
|
||||
chunk = this->requestChunk(chunkIndex);
|
||||
}
|
||||
|
||||
|
||||
if (chunk != nullptr) {
|
||||
// Wait for chunk if not loaded yet
|
||||
if (!chunk->isLoaded && !chunk->isFailed) {
|
||||
BELL_LOG(info, "cspot", "Chunk not loaded, waiting for %d", chunkIndex);
|
||||
chunk->isLoadedSemaphore->wait();
|
||||
}
|
||||
|
||||
if (chunk->isFailed) return 0;
|
||||
|
||||
// Attempt to read from chunk
|
||||
auto read = attemptRead(buf, nbytes, chunk);
|
||||
pos += read;
|
||||
|
||||
auto nextChunkPos = ((chunkIndex + 1) * AUDIO_CHUNK_SIZE) + 1;
|
||||
if (loadAheadEnabled && nextChunkPos < fileSize) {
|
||||
auto nextChunk = getChunkForPosition(nextChunkPos);
|
||||
|
||||
if (nextChunk == nullptr) {
|
||||
// Request next chunk
|
||||
this->requestChunk(chunkIndex + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return read;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t ChunkedByteStream::attemptRead(uint8_t *buffer, size_t bytes, std::shared_ptr<AudioChunk> chunk) {
|
||||
//if (!chunk->isLoaded || chunk->isFailed || chunk->startPosition >= pos || chunk->endPosition < pos) return 0;
|
||||
|
||||
// Calculate how many bytes we can read from chunk
|
||||
auto offset = pos - chunk->startPosition;
|
||||
auto toRead = bytes;
|
||||
if (toRead > chunk->decryptedData.size() - offset) {
|
||||
toRead = chunk->decryptedData.size() - offset;
|
||||
}
|
||||
|
||||
chunk->readData(buffer, offset, toRead);
|
||||
return toRead;
|
||||
}
|
||||
|
||||
void ChunkedByteStream::seek(size_t nbytes) {
|
||||
std::scoped_lock lock(this->readMutex);
|
||||
pos = nbytes;
|
||||
|
||||
|
||||
if (getChunkForPosition(this->pos) == nullptr) {
|
||||
// Seeking might look back - therefore we preload some past data
|
||||
auto startPosition = (this->pos / 4) - (AUDIO_CHUNK_SIZE / 4);
|
||||
|
||||
// AES block size is 16, so the index must be divisible by it
|
||||
while ((startPosition * 4) % 16 != 0)
|
||||
startPosition++; // ik, ugly lol
|
||||
|
||||
this->chunks.push_back(mercuryManager->fetchAudioChunk(fileId, audioKey, startPosition,
|
||||
startPosition + (AUDIO_CHUNK_SIZE / 4)));
|
||||
}
|
||||
}
|
||||
|
||||
size_t ChunkedByteStream::skip(size_t nbytes) {
|
||||
std::scoped_lock lock(this->readMutex);
|
||||
pos += nbytes;
|
||||
return pos;
|
||||
}
|
||||
|
||||
size_t ChunkedByteStream::position() {
|
||||
return pos;
|
||||
}
|
||||
|
||||
size_t ChunkedByteStream::size() {
|
||||
return fileSize;
|
||||
}
|
||||
|
||||
void ChunkedByteStream::close() {
|
||||
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
#include "ConfigJSON.h"
|
||||
#include "JSONObject.h"
|
||||
#include "Logger.h"
|
||||
#include "ConstantParameters.h"
|
||||
|
||||
|
||||
ConfigJSON::ConfigJSON(std::string jsonFileName, std::shared_ptr<FileHelper> file)
|
||||
{
|
||||
_file = file;
|
||||
_jsonFileName = jsonFileName;
|
||||
}
|
||||
|
||||
bool ConfigJSON::load()
|
||||
{
|
||||
// Config filename check
|
||||
if(_jsonFileName.length() > 0)
|
||||
{
|
||||
std::string jsonConfig;
|
||||
_file->readFile(_jsonFileName, jsonConfig);
|
||||
|
||||
// Ignore config if empty
|
||||
if(jsonConfig.length() > 0)
|
||||
{
|
||||
auto root = cJSON_Parse(jsonConfig.c_str());
|
||||
|
||||
if(cJSON_HasObjectItem(root, "deviceName"))
|
||||
{
|
||||
auto deviceNameObject = cJSON_GetObjectItemCaseSensitive(root, "deviceName");
|
||||
this->deviceName = std::string(cJSON_GetStringValue(deviceNameObject));
|
||||
}
|
||||
if(cJSON_HasObjectItem(root, "bitrate"))
|
||||
{
|
||||
auto bitrateObject = cJSON_GetObjectItemCaseSensitive(root, "bitrate");
|
||||
switch((uint16_t)cJSON_GetNumberValue(bitrateObject)){
|
||||
case 320:
|
||||
this->format = AudioFormat_OGG_VORBIS_320;
|
||||
break;
|
||||
case 160:
|
||||
this->format = AudioFormat_OGG_VORBIS_160;
|
||||
break;
|
||||
case 96:
|
||||
this->format = AudioFormat_OGG_VORBIS_96;
|
||||
break;
|
||||
default:
|
||||
this->format = AudioFormat_OGG_VORBIS_320;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(cJSON_HasObjectItem(root, "volume"))
|
||||
{
|
||||
auto volumeObject = cJSON_GetObjectItemCaseSensitive(root, "volume");
|
||||
this->volume = cJSON_GetNumberValue(volumeObject);
|
||||
}
|
||||
cJSON_Delete(root);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Config file not found or invalid
|
||||
// Set default values
|
||||
this->volume = 32767;
|
||||
this->deviceName = defaultDeviceName;
|
||||
this->format = AudioFormat_OGG_VORBIS_160;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool ConfigJSON::save()
|
||||
{
|
||||
bell::JSONObject obj;
|
||||
|
||||
obj["volume"] = this->volume;
|
||||
obj["deviceName"] = this->deviceName;
|
||||
switch(this->format){
|
||||
case AudioFormat_OGG_VORBIS_320:
|
||||
obj["bitrate"] = 320;
|
||||
break;
|
||||
case AudioFormat_OGG_VORBIS_160:
|
||||
obj["bitrate"] = 160;
|
||||
break;
|
||||
case AudioFormat_OGG_VORBIS_96:
|
||||
obj["bitrate"] = 96;
|
||||
break;
|
||||
default:
|
||||
obj["bitrate"] = 160;
|
||||
break;
|
||||
}
|
||||
|
||||
return _file->writeFile(_jsonFileName, obj.toString());
|
||||
}
|
||||
@@ -1,133 +1,201 @@
|
||||
#include "LoginBlob.h"
|
||||
#include "JSONObject.h"
|
||||
#include "ConstantParameters.h"
|
||||
#include "Logger.h"
|
||||
|
||||
LoginBlob::LoginBlob()
|
||||
{
|
||||
this->crypto = std::make_unique<Crypto>();
|
||||
using namespace cspot;
|
||||
|
||||
LoginBlob::LoginBlob(std::string name) {
|
||||
char hash[32];
|
||||
sprintf(hash, "%016zu", std::hash<std::string>{}(name));
|
||||
// base is 142137fd329622137a14901634264e6f332e2411
|
||||
this->deviceId = std::string("142137fd329622137a149016") + std::string(hash);
|
||||
this->crypto = std::make_unique<Crypto>();
|
||||
this->name = name;
|
||||
|
||||
this->crypto->dhInit();
|
||||
}
|
||||
|
||||
std::vector<uint8_t> LoginBlob::decodeBlob(const std::vector<uint8_t> &blob, const std::vector<uint8_t> &sharedKey)
|
||||
{
|
||||
// 0:16 - iv; 17:-20 - blob; -20:0 - checksum
|
||||
auto iv = std::vector<uint8_t>(blob.begin(), blob.begin() + 16);
|
||||
auto encrypted = std::vector<uint8_t>(blob.begin() + 16, blob.end() - 20);
|
||||
auto checksum = std::vector<uint8_t>(blob.end() - 20, blob.end());
|
||||
std::vector<uint8_t> LoginBlob::decodeBlob(
|
||||
const std::vector<uint8_t>& blob, const std::vector<uint8_t>& sharedKey) {
|
||||
// 0:16 - iv; 17:-20 - blob; -20:0 - checksum
|
||||
auto iv = std::vector<uint8_t>(blob.begin(), blob.begin() + 16);
|
||||
auto encrypted = std::vector<uint8_t>(blob.begin() + 16, blob.end() - 20);
|
||||
auto checksum = std::vector<uint8_t>(blob.end() - 20, blob.end());
|
||||
|
||||
// baseKey = sha1(sharedKey) 0:16
|
||||
crypto->sha1Init();
|
||||
crypto->sha1Update(sharedKey);
|
||||
auto baseKey = crypto->sha1FinalBytes();
|
||||
baseKey = std::vector<uint8_t>(baseKey.begin(), baseKey.begin() + 16);
|
||||
// baseKey = sha1(sharedKey) 0:16
|
||||
crypto->sha1Init();
|
||||
|
||||
auto checksumMessage = std::string("checksum");
|
||||
auto checksumKey = crypto->sha1HMAC(baseKey, std::vector<uint8_t>(checksumMessage.begin(), checksumMessage.end()));
|
||||
crypto->sha1Update(sharedKey);
|
||||
auto baseKey = crypto->sha1FinalBytes();
|
||||
baseKey = std::vector<uint8_t>(baseKey.begin(), baseKey.begin() + 16);
|
||||
|
||||
auto encryptionMessage = std::string("encryption");
|
||||
auto encryptionKey = crypto->sha1HMAC(baseKey, std::vector<uint8_t>(encryptionMessage.begin(), encryptionMessage.end()));
|
||||
auto checksumMessage = std::string("checksum");
|
||||
auto checksumKey = crypto->sha1HMAC(
|
||||
baseKey,
|
||||
std::vector<uint8_t>(checksumMessage.begin(), checksumMessage.end()));
|
||||
|
||||
auto mac = crypto->sha1HMAC(checksumKey, encrypted);
|
||||
auto encryptionMessage = std::string("encryption");
|
||||
auto encryptionKey = crypto->sha1HMAC(
|
||||
baseKey,
|
||||
std::vector<uint8_t>(encryptionMessage.begin(), encryptionMessage.end()));
|
||||
|
||||
// Check checksum
|
||||
if (mac != checksum)
|
||||
{
|
||||
CSPOT_LOG(error, "Mac doesn't match!" );
|
||||
}
|
||||
auto mac = crypto->sha1HMAC(checksumKey, encrypted);
|
||||
|
||||
encryptionKey = std::vector<uint8_t>(encryptionKey.begin(), encryptionKey.begin() + 16);
|
||||
crypto->aesCTRXcrypt(encryptionKey, iv, encrypted.data(), encrypted.size());
|
||||
// Check checksum
|
||||
if (mac != checksum) {
|
||||
CSPOT_LOG(error, "Mac doesn't match!");
|
||||
}
|
||||
|
||||
return encrypted;
|
||||
encryptionKey =
|
||||
std::vector<uint8_t>(encryptionKey.begin(), encryptionKey.begin() + 16);
|
||||
crypto->aesCTRXcrypt(encryptionKey, iv, encrypted.data(), encrypted.size());
|
||||
|
||||
return encrypted;
|
||||
}
|
||||
|
||||
uint32_t LoginBlob::readBlobInt(const std::vector<uint8_t> &data)
|
||||
{
|
||||
auto lo = data[blobSkipPosition];
|
||||
if ((int)(lo & 0x80) == 0)
|
||||
{
|
||||
this->blobSkipPosition += 1;
|
||||
return lo;
|
||||
}
|
||||
uint32_t LoginBlob::readBlobInt(const std::vector<uint8_t>& data) {
|
||||
auto lo = data[blobSkipPosition];
|
||||
if ((int)(lo & 0x80) == 0) {
|
||||
this->blobSkipPosition += 1;
|
||||
return lo;
|
||||
}
|
||||
|
||||
auto hi = data[blobSkipPosition + 1];
|
||||
this->blobSkipPosition += 2;
|
||||
auto hi = data[blobSkipPosition + 1];
|
||||
this->blobSkipPosition += 2;
|
||||
|
||||
return (uint32_t)((lo & 0x7f) | (hi << 7));
|
||||
return (uint32_t)((lo & 0x7f) | (hi << 7));
|
||||
}
|
||||
|
||||
std::vector<uint8_t> LoginBlob::decodeBlobSecondary(const std::vector<uint8_t> &blob, const std::string &username, const std::string &deviceId)
|
||||
{
|
||||
auto encryptedString = std::string(blob.begin(), blob.end());
|
||||
auto blobData = crypto->base64Decode(encryptedString);
|
||||
std::vector<uint8_t> LoginBlob::decodeBlobSecondary(
|
||||
const std::vector<uint8_t>& blob, const std::string& username,
|
||||
const std::string& deviceId) {
|
||||
auto encryptedString = std::string(blob.begin(), blob.end());
|
||||
auto blobData = crypto->base64Decode(encryptedString);
|
||||
|
||||
crypto->sha1Init();
|
||||
crypto->sha1Update(std::vector<uint8_t>(deviceId.begin(), deviceId.end()));
|
||||
auto secret = crypto->sha1FinalBytes();
|
||||
auto pkBaseKey = crypto->pbkdf2HmacSha1(secret, std::vector<uint8_t>(username.begin(), username.end()), 256, 20);
|
||||
crypto->sha1Init();
|
||||
crypto->sha1Update(std::vector<uint8_t>(deviceId.begin(), deviceId.end()));
|
||||
auto secret = crypto->sha1FinalBytes();
|
||||
auto pkBaseKey = crypto->pbkdf2HmacSha1(
|
||||
secret, std::vector<uint8_t>(username.begin(), username.end()), 256, 20);
|
||||
|
||||
crypto->sha1Init();
|
||||
crypto->sha1Update(pkBaseKey);
|
||||
auto key = std::vector<uint8_t>({0x00, 0x00, 0x00, 0x14}); // len of base key
|
||||
auto baseKeyHashed = crypto->sha1FinalBytes();
|
||||
key.insert(key.begin(), baseKeyHashed.begin(), baseKeyHashed.end());
|
||||
crypto->sha1Init();
|
||||
crypto->sha1Update(pkBaseKey);
|
||||
auto key = std::vector<uint8_t>({0x00, 0x00, 0x00, 0x14}); // len of base key
|
||||
auto baseKeyHashed = crypto->sha1FinalBytes();
|
||||
key.insert(key.begin(), baseKeyHashed.begin(), baseKeyHashed.end());
|
||||
|
||||
crypto->aesECBdecrypt(key, blobData);
|
||||
crypto->aesECBdecrypt(key, blobData);
|
||||
|
||||
auto l = blobData.size();
|
||||
auto l = blobData.size();
|
||||
|
||||
for (int i = 0; i < l - 16; i++)
|
||||
{
|
||||
blobData[l - i - 1] ^= blobData[l - i - 17];
|
||||
}
|
||||
for (int i = 0; i < l - 16; i++) {
|
||||
blobData[l - i - 1] ^= blobData[l - i - 17];
|
||||
}
|
||||
|
||||
return blobData;
|
||||
return blobData;
|
||||
}
|
||||
|
||||
void LoginBlob::loadZeroconf(const std::vector<uint8_t> &blob, const std::vector<uint8_t> &sharedKey, const std::string &deviceId, const std::string &username)
|
||||
{
|
||||
auto partDecoded = this->decodeBlob(blob, sharedKey);
|
||||
auto loginData = this->decodeBlobSecondary(partDecoded, username, deviceId);
|
||||
void LoginBlob::loadZeroconf(const std::vector<uint8_t>& blob,
|
||||
const std::vector<uint8_t>& sharedKey,
|
||||
const std::string& deviceId,
|
||||
const std::string& username) {
|
||||
|
||||
// Parse blob
|
||||
blobSkipPosition = 1;
|
||||
blobSkipPosition += readBlobInt(loginData);
|
||||
blobSkipPosition += 1;
|
||||
this->authType = readBlobInt(loginData);
|
||||
blobSkipPosition += 1;
|
||||
auto authSize = readBlobInt(loginData);
|
||||
this->username = username;
|
||||
this->authData = std::vector<uint8_t>(loginData.begin() + blobSkipPosition, loginData.begin() + blobSkipPosition + authSize);
|
||||
auto partDecoded = this->decodeBlob(blob, sharedKey);
|
||||
auto loginData = this->decodeBlobSecondary(partDecoded, username, deviceId);
|
||||
|
||||
// Parse blob
|
||||
blobSkipPosition = 1;
|
||||
blobSkipPosition += readBlobInt(loginData);
|
||||
blobSkipPosition += 1;
|
||||
this->authType = readBlobInt(loginData);
|
||||
blobSkipPosition += 1;
|
||||
auto authSize = readBlobInt(loginData);
|
||||
this->username = username;
|
||||
this->authData =
|
||||
std::vector<uint8_t>(loginData.begin() + blobSkipPosition,
|
||||
loginData.begin() + blobSkipPosition + authSize);
|
||||
}
|
||||
|
||||
void LoginBlob::loadUserPass(const std::string &username, const std::string &password)
|
||||
{
|
||||
this->username = username;
|
||||
this->authData = std::vector<uint8_t>(password.begin(), password.end());
|
||||
this->authType = static_cast<uint32_t>(AuthenticationType_AUTHENTICATION_USER_PASS);
|
||||
void LoginBlob::loadUserPass(const std::string& username,
|
||||
const std::string& password) {
|
||||
this->username = username;
|
||||
this->authData = std::vector<uint8_t>(password.begin(), password.end());
|
||||
this->authType =
|
||||
static_cast<uint32_t>(AuthenticationType_AUTHENTICATION_USER_PASS);
|
||||
}
|
||||
|
||||
void LoginBlob::loadJson(const std::string &json)
|
||||
{
|
||||
auto root = cJSON_Parse(json.c_str());
|
||||
auto authTypeObject = cJSON_GetObjectItemCaseSensitive(root, "authType");
|
||||
auto usernameObject = cJSON_GetObjectItemCaseSensitive(root, "username");
|
||||
auto authDataObject = cJSON_GetObjectItemCaseSensitive(root, "authData");
|
||||
void LoginBlob::loadJson(const std::string& json) {
|
||||
auto root = nlohmann::json::parse(json);
|
||||
this->authType = root["authType"];
|
||||
this->username = root["username"];
|
||||
std::string authDataObject = root["authData"];
|
||||
|
||||
auto authDataString = std::string(cJSON_GetStringValue(authDataObject));
|
||||
this->authData = crypto->base64Decode(authDataString);
|
||||
|
||||
this->username = std::string(cJSON_GetStringValue(usernameObject));
|
||||
this->authType = cJSON_GetNumberValue(authTypeObject);
|
||||
|
||||
cJSON_Delete(root);
|
||||
this->authData = crypto->base64Decode(authDataObject);
|
||||
}
|
||||
|
||||
std::string LoginBlob::toJson()
|
||||
{
|
||||
bell::JSONObject obj;
|
||||
obj["authData"] = crypto->base64Encode(authData);
|
||||
obj["authType"] = this->authType;
|
||||
obj["username"] = this->username;
|
||||
|
||||
return obj.toString();
|
||||
std::string LoginBlob::toJson() {
|
||||
nlohmann::json obj;
|
||||
obj["authData"] = crypto->base64Encode(authData);
|
||||
obj["authType"] = this->authType;
|
||||
obj["username"] = this->username;
|
||||
|
||||
return obj.dump();
|
||||
}
|
||||
|
||||
void LoginBlob::loadZeroconfQuery(
|
||||
std::map<std::string, std::string>& queryParams) {
|
||||
// Get all urlencoded params
|
||||
auto username = queryParams["userName"];
|
||||
auto blobString = queryParams["blob"];
|
||||
auto clientKeyString = queryParams["clientKey"];
|
||||
auto deviceName = queryParams["deviceName"];
|
||||
|
||||
// client key and bytes are urlencoded
|
||||
auto clientKeyBytes = crypto->base64Decode(clientKeyString);
|
||||
auto blobBytes = crypto->base64Decode(blobString);
|
||||
|
||||
// Generated secret based on earlier generated DH
|
||||
auto secretKey = crypto->dhCalculateShared(clientKeyBytes);
|
||||
|
||||
this->loadZeroconf(blobBytes, secretKey, deviceId, username);
|
||||
}
|
||||
|
||||
std::string LoginBlob::buildZeroconfInfo() {
|
||||
// Encode publicKey into base64
|
||||
|
||||
auto encodedKey = crypto->base64Encode(crypto->publicKey);
|
||||
|
||||
nlohmann::json obj;
|
||||
obj["status"] = 101;
|
||||
obj["statusString"] = "OK";
|
||||
obj["version"] = cspot::protocolVersion;
|
||||
obj["spotifyError"] = 0;
|
||||
obj["libraryVersion"] = cspot::swVersion;
|
||||
obj["accountReq"] = "PREMIUM";
|
||||
obj["brandDisplayName"] = cspot::brandName;
|
||||
obj["modelDisplayName"] = name;
|
||||
obj["voiceSupport"] = "NO";
|
||||
obj["availability"] = this->username;
|
||||
obj["productID"] = 0;
|
||||
obj["tokenType"] = "default";
|
||||
obj["groupStatus"] = "NONE";
|
||||
obj["resolverVersion"] = "0";
|
||||
obj["scope"] = "streaming,client-authorization-universal";
|
||||
obj["activeUser"] = "";
|
||||
obj["deviceID"] = deviceId;
|
||||
obj["remoteName"] = name;
|
||||
obj["publicKey"] = encodedKey;
|
||||
obj["deviceType"] = "SPEAKER";
|
||||
|
||||
return obj.dump();
|
||||
}
|
||||
|
||||
std::string LoginBlob::getDeviceId() {
|
||||
return this->deviceId;
|
||||
}
|
||||
std::string LoginBlob::getDeviceName() {
|
||||
return this->name;
|
||||
}
|
||||
std::string LoginBlob::getUserName() {
|
||||
return this->username;
|
||||
}
|
||||
|
||||
@@ -1,373 +0,0 @@
|
||||
#include "MercuryManager.h"
|
||||
#include <iostream>
|
||||
#include "Logger.h"
|
||||
|
||||
std::map<MercuryType, std::string> MercuryTypeMap({
|
||||
{MercuryType::GET, "GET"},
|
||||
{MercuryType::SEND, "SEND"},
|
||||
{MercuryType::SUB, "SUB"},
|
||||
{MercuryType::UNSUB, "UNSUB"},
|
||||
});
|
||||
|
||||
MercuryManager::MercuryManager(std::unique_ptr<Session> session): bell::Task("mercuryManager", 6 * 1024, 1, 1)
|
||||
{
|
||||
tempMercuryHeader = {};
|
||||
this->timeProvider = std::make_shared<TimeProvider>();
|
||||
this->callbacks = std::map<uint64_t, mercuryCallback>();
|
||||
this->subscriptions = std::map<std::string, mercuryCallback>();
|
||||
this->session = std::move(session);
|
||||
this->sequenceId = 0x00000001;
|
||||
this->audioChunkManager = std::make_unique<AudioChunkManager>();
|
||||
this->audioChunkSequence = 0;
|
||||
this->audioKeySequence = 0;
|
||||
this->queue = std::vector<std::unique_ptr<Packet>>();
|
||||
queueSemaphore = std::make_unique<WrappedSemaphore>(200);
|
||||
|
||||
this->session->shanConn->conn->timeoutHandler = [this]() {
|
||||
return this->timeoutHandler();
|
||||
};
|
||||
}
|
||||
|
||||
MercuryManager::~MercuryManager()
|
||||
{
|
||||
//pb_release(Header_fields, &tempMercuryHeader);
|
||||
}
|
||||
|
||||
bool MercuryManager::timeoutHandler()
|
||||
{
|
||||
if (!isRunning) return true;
|
||||
|
||||
auto currentTimestamp = timeProvider->getSyncedTimestamp();
|
||||
|
||||
if (this->lastRequestTimestamp != -1 && currentTimestamp - this->lastRequestTimestamp > AUDIOCHUNK_TIMEOUT_MS)
|
||||
{
|
||||
CSPOT_LOG(debug, "Reconnection required, no mercury response");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (currentTimestamp - this->lastPingTimestamp > PING_TIMEOUT_MS)
|
||||
{
|
||||
CSPOT_LOG(debug, "Reconnection required, no ping received");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void MercuryManager::unregisterMercuryCallback(uint64_t seqId)
|
||||
{
|
||||
auto element = this->callbacks.find(seqId);
|
||||
if (element != this->callbacks.end())
|
||||
{
|
||||
this->callbacks.erase(element);
|
||||
}
|
||||
}
|
||||
|
||||
void MercuryManager::requestAudioKey(std::vector<uint8_t> trackId, std::vector<uint8_t> fileId, audioKeyCallback& audioCallback)
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(reconnectionMutex);
|
||||
auto buffer = fileId;
|
||||
this->keyCallback = audioCallback;
|
||||
// Structure: [FILEID] [TRACKID] [4 BYTES SEQUENCE ID] [0x00, 0x00]
|
||||
buffer.insert(buffer.end(), trackId.begin(), trackId.end());
|
||||
auto audioKeySequence = pack<uint32_t>(htonl(this->audioKeySequence));
|
||||
buffer.insert(buffer.end(), audioKeySequence.begin(), audioKeySequence.end());
|
||||
auto suffix = std::vector<uint8_t>({ 0x00, 0x00 });
|
||||
buffer.insert(buffer.end(), suffix.begin(), suffix.end());
|
||||
|
||||
// Bump audio key sequence
|
||||
this->audioKeySequence += 1;
|
||||
|
||||
// Used for broken connection detection
|
||||
this->lastRequestTimestamp = timeProvider->getSyncedTimestamp();
|
||||
this->session->shanConn->sendPacket(static_cast<uint8_t>(MercuryType::AUDIO_KEY_REQUEST_COMMAND), buffer);
|
||||
}
|
||||
|
||||
void MercuryManager::freeAudioKeyCallback()
|
||||
{
|
||||
this->keyCallback = nullptr;
|
||||
}
|
||||
|
||||
std::shared_ptr<AudioChunk> MercuryManager::fetchAudioChunk(std::vector<uint8_t> fileId, std::vector<uint8_t>& audioKey, uint16_t index)
|
||||
{
|
||||
return this->fetchAudioChunk(fileId, audioKey, index * AUDIO_CHUNK_SIZE / 4, (index + 1) * AUDIO_CHUNK_SIZE / 4);
|
||||
}
|
||||
|
||||
std::shared_ptr<AudioChunk> MercuryManager::fetchAudioChunk(std::vector<uint8_t> fileId, std::vector<uint8_t>& audioKey, uint32_t startPos, uint32_t endPos)
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(reconnectionMutex);
|
||||
auto sampleStartBytes = pack<uint32_t>(htonl(startPos));
|
||||
auto sampleEndBytes = pack<uint32_t>(htonl(endPos));
|
||||
|
||||
auto buffer = pack<uint16_t>(htons(this->audioChunkSequence));
|
||||
auto hardcodedData = std::vector<uint8_t>(
|
||||
{ 0x00, 0x01, // Channel num, currently just hardcoded to 1
|
||||
0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, // bytes magic
|
||||
0x00, 0x00, 0x9C, 0x40,
|
||||
0x00, 0x02, 0x00, 0x00 });
|
||||
buffer.insert(buffer.end(), hardcodedData.begin(), hardcodedData.end());
|
||||
buffer.insert(buffer.end(), fileId.begin(), fileId.end());
|
||||
buffer.insert(buffer.end(), sampleStartBytes.begin(), sampleStartBytes.end());
|
||||
buffer.insert(buffer.end(), sampleEndBytes.begin(), sampleEndBytes.end());
|
||||
|
||||
// Bump chunk sequence
|
||||
this->audioChunkSequence += 1;
|
||||
this->session->shanConn->sendPacket(static_cast<uint8_t>(MercuryType::AUDIO_CHUNK_REQUEST_COMMAND), buffer);
|
||||
|
||||
// Used for broken connection detection
|
||||
//CSPOT_LOG(info, "requesting Chunk %hu", this->audioChunkSequence - 1);
|
||||
this->lastRequestTimestamp = this->timeProvider->getSyncedTimestamp();
|
||||
return this->audioChunkManager->registerNewChunk(this->audioChunkSequence - 1, audioKey, startPos, endPos);
|
||||
}
|
||||
|
||||
void MercuryManager::reconnect()
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(this->reconnectionMutex);
|
||||
this->lastPingTimestamp = -1;
|
||||
this->lastRequestTimestamp = -1;
|
||||
RECONNECT:
|
||||
if (!isRunning) return;
|
||||
CSPOT_LOG(debug, "Trying to reconnect...");
|
||||
try
|
||||
{
|
||||
if (this->session->shanConn->conn != nullptr)
|
||||
{
|
||||
this->session->shanConn->conn->timeoutHandler = nullptr;
|
||||
}
|
||||
this->audioChunkManager->failAllChunks();
|
||||
if (this->session->authBlob != nullptr)
|
||||
{
|
||||
this->lastAuthBlob = this->session->authBlob;
|
||||
}
|
||||
this->session = std::make_unique<Session>();
|
||||
this->session->connectWithRandomAp();
|
||||
this->session->authenticate(this->lastAuthBlob);
|
||||
this->session->shanConn->conn->timeoutHandler = [this]() {
|
||||
return this->timeoutHandler();
|
||||
};
|
||||
CSPOT_LOG(debug, "Reconnected successfuly :)");
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
CSPOT_LOG(debug, "Reconnection failed, willl retry in %d secs", RECONNECTION_RETRY_MS / 1000);
|
||||
usleep(RECONNECTION_RETRY_MS * 1000);
|
||||
goto RECONNECT;
|
||||
//reconnect();
|
||||
}
|
||||
}
|
||||
|
||||
void MercuryManager::runTask()
|
||||
{
|
||||
std::scoped_lock lock(this->runningMutex);
|
||||
// Listen for mercury replies and handle them accordingly
|
||||
isRunning = true;
|
||||
while (isRunning)
|
||||
{
|
||||
std::unique_ptr<Packet> packet;
|
||||
try
|
||||
{
|
||||
packet = this->session->shanConn->recvPacket();
|
||||
}
|
||||
catch (const std::runtime_error& e)
|
||||
{
|
||||
if (!isRunning) break;
|
||||
// Reconnection required
|
||||
this->reconnect();
|
||||
this->reconnectedCallback();
|
||||
continue;
|
||||
}
|
||||
if (static_cast<MercuryType>(packet->command) == MercuryType::PING) // @TODO: Handle time synchronization through ping
|
||||
{
|
||||
this->timeProvider->syncWithPingPacket(packet->data);
|
||||
|
||||
this->lastPingTimestamp = this->timeProvider->getSyncedTimestamp();
|
||||
this->session->shanConn->sendPacket(0x49, packet->data);
|
||||
}
|
||||
else if (static_cast<MercuryType>(packet->command) == MercuryType::AUDIO_CHUNK_SUCCESS_RESPONSE)
|
||||
{
|
||||
this->lastRequestTimestamp = -1;
|
||||
this->audioChunkManager->handleChunkData(packet->data, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
this->queue.push_back(std::move(packet));
|
||||
this->queueSemaphore->give();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MercuryManager::stop() {
|
||||
std::scoped_lock stop(this->stopMutex);
|
||||
CSPOT_LOG(debug, "Stopping mercury manager");
|
||||
isRunning = false;
|
||||
audioChunkManager->close();
|
||||
std::scoped_lock lock(this->runningMutex);
|
||||
CSPOT_LOG(debug, "mercury stopped");
|
||||
}
|
||||
|
||||
void MercuryManager::updateQueue() {
|
||||
if (queueSemaphore->twait() == 0) {
|
||||
if (this->queue.size() > 0)
|
||||
{
|
||||
std::unique_ptr<Packet> packet = std::move(this->queue[0]);
|
||||
this->queue.erase(this->queue.begin());
|
||||
if(packet == nullptr){
|
||||
return;
|
||||
}
|
||||
CSPOT_LOG(debug, "Received packet with code %d of length %d", packet->command, packet->data.size());
|
||||
switch (static_cast<MercuryType>(packet->command))
|
||||
{
|
||||
case MercuryType::COUNTRY_CODE_RESPONSE:
|
||||
{
|
||||
|
||||
memcpy(countryCode, packet->data.data(), 2);
|
||||
CSPOT_LOG(debug, "Received country code: %.2s", countryCode);
|
||||
break;
|
||||
}
|
||||
case MercuryType::AUDIO_KEY_FAILURE_RESPONSE:
|
||||
case MercuryType::AUDIO_KEY_SUCCESS_RESPONSE:
|
||||
{
|
||||
this->lastRequestTimestamp = -1;
|
||||
|
||||
// First four bytes mark the sequence id
|
||||
auto seqId = ntohl(extract<uint32_t>(packet->data, 0));
|
||||
if (seqId == (this->audioKeySequence - 1) && this->keyCallback != nullptr)
|
||||
{
|
||||
auto success = static_cast<MercuryType>(packet->command) == MercuryType::AUDIO_KEY_SUCCESS_RESPONSE;
|
||||
this->keyCallback(success, packet->data);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MercuryType::AUDIO_CHUNK_FAILURE_RESPONSE:
|
||||
{
|
||||
CSPOT_LOG(error, "Audio Chunk failure!");
|
||||
this->audioChunkManager->handleChunkData(packet->data, true);
|
||||
this->lastRequestTimestamp = -1;
|
||||
break;
|
||||
}
|
||||
case MercuryType::SEND:
|
||||
case MercuryType::SUB:
|
||||
case MercuryType::UNSUB:
|
||||
{
|
||||
auto response = std::make_unique<MercuryResponse>(packet->data);
|
||||
if (response->parts.size() > 0)
|
||||
{
|
||||
CSPOT_LOG(debug, " MercuryType::UNSUB response->parts[0].size() = %d", response->parts[0].size());
|
||||
}
|
||||
if (this->callbacks.count(response->sequenceId) > 0)
|
||||
{
|
||||
auto seqId = response->sequenceId;
|
||||
this->callbacks[response->sequenceId](std::move(response));
|
||||
this->callbacks.erase(this->callbacks.find(seqId));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MercuryType::SUBRES:
|
||||
{
|
||||
auto response = std::make_unique<MercuryResponse>(packet->data);
|
||||
|
||||
auto uri = std::string(response->mercuryHeader.uri);
|
||||
if (this->subscriptions.count(uri) > 0)
|
||||
{
|
||||
this->subscriptions[uri](std::move(response));
|
||||
//this->subscriptions.erase(std::string(response->mercuryHeader.uri));
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MercuryManager::handleQueue()
|
||||
{
|
||||
while (isRunning)
|
||||
{
|
||||
this->updateQueue();
|
||||
}
|
||||
|
||||
std::scoped_lock lock(this->stopMutex);
|
||||
}
|
||||
|
||||
uint64_t MercuryManager::execute(MercuryType method, std::string uri, mercuryCallback& callback, mercuryCallback& subscription, mercuryParts& payload)
|
||||
{
|
||||
if (!isRunning) return -1;
|
||||
std::lock_guard<std::mutex> guard(reconnectionMutex);
|
||||
// Construct mercury header
|
||||
|
||||
CSPOT_LOG(debug, "executing MercuryType %s", MercuryTypeMap[method].c_str());
|
||||
pbPutString(uri, tempMercuryHeader.uri);
|
||||
pbPutString(MercuryTypeMap[method], tempMercuryHeader.method);
|
||||
|
||||
tempMercuryHeader.has_method = true;
|
||||
tempMercuryHeader.has_uri = true;
|
||||
|
||||
// GET and SEND are actually the same. Therefore the override
|
||||
// The difference between them is only in header's method
|
||||
if (method == MercuryType::GET)
|
||||
{
|
||||
method = MercuryType::SEND;
|
||||
}
|
||||
|
||||
auto headerBytes = pbEncode(Header_fields, &tempMercuryHeader);
|
||||
|
||||
// Register a subscription when given method is called
|
||||
if (method == MercuryType::SUB)
|
||||
{
|
||||
this->subscriptions.insert({ uri, subscription });
|
||||
}
|
||||
|
||||
this->callbacks.insert({ sequenceId, callback });
|
||||
|
||||
// Structure: [Sequence size] [SequenceId] [0x1] [Payloads number]
|
||||
// [Header size] [Header] [Payloads (size + data)]
|
||||
|
||||
// Pack sequenceId
|
||||
auto sequenceIdBytes = pack<uint64_t>(hton64(this->sequenceId));
|
||||
auto sequenceSizeBytes = pack<uint16_t>(htons(sequenceIdBytes.size()));
|
||||
|
||||
sequenceIdBytes.insert(sequenceIdBytes.begin(), sequenceSizeBytes.begin(), sequenceSizeBytes.end());
|
||||
sequenceIdBytes.push_back(0x01);
|
||||
|
||||
auto payloadNum = pack<uint16_t>(htons(payload.size() + 1));
|
||||
sequenceIdBytes.insert(sequenceIdBytes.end(), payloadNum.begin(), payloadNum.end());
|
||||
|
||||
auto headerSizePayload = pack<uint16_t>(htons(headerBytes.size()));
|
||||
sequenceIdBytes.insert(sequenceIdBytes.end(), headerSizePayload.begin(), headerSizePayload.end());
|
||||
sequenceIdBytes.insert(sequenceIdBytes.end(), headerBytes.begin(), headerBytes.end());
|
||||
|
||||
// Encode all the payload parts
|
||||
for (int x = 0; x < payload.size(); x++)
|
||||
{
|
||||
headerSizePayload = pack<uint16_t>(htons(payload[x].size()));
|
||||
sequenceIdBytes.insert(sequenceIdBytes.end(), headerSizePayload.begin(), headerSizePayload.end());
|
||||
sequenceIdBytes.insert(sequenceIdBytes.end(), payload[x].begin(), payload[x].end());
|
||||
}
|
||||
|
||||
// Bump sequence id
|
||||
this->sequenceId += 1;
|
||||
|
||||
this->session->shanConn->sendPacket(static_cast<std::underlying_type<MercuryType>::type>(method), sequenceIdBytes);
|
||||
|
||||
return this->sequenceId - 1;
|
||||
}
|
||||
|
||||
uint64_t MercuryManager::execute(MercuryType method, std::string uri, mercuryCallback& callback, mercuryParts& payload)
|
||||
{
|
||||
mercuryCallback subscription = nullptr;
|
||||
return this->execute(method, uri, callback, subscription, payload);
|
||||
}
|
||||
|
||||
uint64_t MercuryManager::execute(MercuryType method, std::string uri, mercuryCallback& callback, mercuryCallback& subscription)
|
||||
{
|
||||
auto payload = mercuryParts(0);
|
||||
return this->execute(method, uri, callback, subscription, payload);
|
||||
}
|
||||
|
||||
uint64_t MercuryManager::execute(MercuryType method, std::string uri, mercuryCallback& callback)
|
||||
{
|
||||
auto payload = mercuryParts(0);
|
||||
return this->execute(method, uri, callback, payload);
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
#include "MercuryResponse.h"
|
||||
|
||||
MercuryResponse::MercuryResponse(std::vector<uint8_t> &data)
|
||||
{
|
||||
// this->mercuryHeader = std::make_unique<Header>();
|
||||
this->mercuryHeader = {};
|
||||
this->parts = mercuryParts(0);
|
||||
this->parseResponse(data);
|
||||
}
|
||||
|
||||
MercuryResponse::~MercuryResponse() {
|
||||
}
|
||||
|
||||
void MercuryResponse::parseResponse(std::vector<uint8_t> &data)
|
||||
{
|
||||
auto sequenceLength = ntohs(extract<uint16_t>(data, 0));
|
||||
this->sequenceId = hton64(extract<uint64_t>(data, 2));
|
||||
|
||||
auto partsNumber = ntohs(extract<uint16_t>(data, 11));
|
||||
|
||||
auto headerSize = ntohs(extract<uint16_t>(data, 13));
|
||||
auto headerBytes = std::vector<uint8_t>(data.begin() + 15, data.begin() + 15 + headerSize);
|
||||
|
||||
auto pos = 15 + headerSize;
|
||||
while (pos < data.size())
|
||||
{
|
||||
auto partSize = ntohs(extract<uint16_t>(data, pos));
|
||||
|
||||
this->parts.push_back(
|
||||
std::vector<uint8_t>(
|
||||
data.begin() + pos + 2,
|
||||
data.begin() + pos + 2 + partSize));
|
||||
pos += 2 + partSize;
|
||||
}
|
||||
|
||||
pb_release(Header_fields, &this->mercuryHeader);
|
||||
pbDecode(this->mercuryHeader, Header_fields, headerBytes);
|
||||
}
|
||||
306
components/spotify/cspot/src/MercurySession.cpp
Normal file
306
components/spotify/cspot/src/MercurySession.cpp
Normal file
@@ -0,0 +1,306 @@
|
||||
#include "MercurySession.h"
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include "BellLogger.h"
|
||||
#include "BellTask.h"
|
||||
#include "BellUtils.h"
|
||||
#include "CSpotContext.h"
|
||||
#include "Logger.h"
|
||||
|
||||
using namespace cspot;
|
||||
|
||||
MercurySession::MercurySession(std::shared_ptr<TimeProvider> timeProvider)
|
||||
: bell::Task("mercury_dispatcher", 4 * 1024, 3, 1) {
|
||||
this->timeProvider = timeProvider;
|
||||
}
|
||||
|
||||
MercurySession::~MercurySession() {
|
||||
std::scoped_lock lock(this->isRunningMutex);
|
||||
}
|
||||
|
||||
void MercurySession::runTask() {
|
||||
isRunning = true;
|
||||
std::scoped_lock lock(this->isRunningMutex);
|
||||
|
||||
this->executeEstabilishedCallback = true;
|
||||
while (isRunning) {
|
||||
cspot::Packet packet = {};
|
||||
try {
|
||||
packet = shanConn->recvPacket();
|
||||
CSPOT_LOG(info, "Received packet, command: %d", packet.command);
|
||||
|
||||
if (static_cast<RequestType>(packet.command) == RequestType::PING) {
|
||||
timeProvider->syncWithPingPacket(packet.data);
|
||||
|
||||
this->lastPingTimestamp = timeProvider->getSyncedTimestamp();
|
||||
this->shanConn->sendPacket(0x49, packet.data);
|
||||
} else {
|
||||
this->packetQueue.push(packet);
|
||||
}
|
||||
} catch (const std::runtime_error& e) {
|
||||
CSPOT_LOG(error, "Error while receiving packet: %s", e.what());
|
||||
failAllPending();
|
||||
|
||||
if (!isRunning)
|
||||
return;
|
||||
|
||||
reconnect();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MercurySession::reconnect() {
|
||||
isReconnecting = true;
|
||||
|
||||
try {
|
||||
this->conn = nullptr;
|
||||
this->shanConn = nullptr;
|
||||
|
||||
this->connectWithRandomAp();
|
||||
this->authenticate(this->authBlob);
|
||||
|
||||
CSPOT_LOG(info, "Reconnection successful");
|
||||
|
||||
BELL_SLEEP_MS(100);
|
||||
|
||||
lastPingTimestamp = timeProvider->getSyncedTimestamp();
|
||||
isReconnecting = false;
|
||||
|
||||
this->executeEstabilishedCallback = true;
|
||||
} catch (...) {
|
||||
CSPOT_LOG(error, "Cannot reconnect, will retry in 5s");
|
||||
BELL_SLEEP_MS(5000);
|
||||
|
||||
if (isRunning) {
|
||||
return reconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MercurySession::setConnectedHandler(
|
||||
ConnectionEstabilishedCallback callback) {
|
||||
this->connectionReadyCallback = callback;
|
||||
}
|
||||
|
||||
bool MercurySession::triggerTimeout() {
|
||||
if (!isRunning)
|
||||
return true;
|
||||
auto currentTimestamp = timeProvider->getSyncedTimestamp();
|
||||
|
||||
if (currentTimestamp - this->lastPingTimestamp > PING_TIMEOUT_MS) {
|
||||
CSPOT_LOG(debug, "Reconnection required, no ping received");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void MercurySession::disconnect() {
|
||||
CSPOT_LOG(info, "Disconnecting mercury session");
|
||||
this->isRunning = false;
|
||||
conn->close();
|
||||
std::scoped_lock lock(this->isRunningMutex);
|
||||
}
|
||||
|
||||
std::string MercurySession::getCountryCode() {
|
||||
return this->countryCode;
|
||||
}
|
||||
|
||||
void MercurySession::handlePacket() {
|
||||
Packet packet = {};
|
||||
|
||||
this->packetQueue.wtpop(packet, 200);
|
||||
|
||||
if (executeEstabilishedCallback && this->connectionReadyCallback != nullptr) {
|
||||
executeEstabilishedCallback = false;
|
||||
this->connectionReadyCallback();
|
||||
}
|
||||
|
||||
switch (static_cast<RequestType>(packet.command)) {
|
||||
case RequestType::COUNTRY_CODE_RESPONSE: {
|
||||
this->countryCode = std::string();
|
||||
this->countryCode.reserve(2);
|
||||
memcpy(this->countryCode.data(), packet.data.data(), 2);
|
||||
CSPOT_LOG(debug, "Received country code");
|
||||
break;
|
||||
}
|
||||
case RequestType::AUDIO_KEY_FAILURE_RESPONSE:
|
||||
case RequestType::AUDIO_KEY_SUCCESS_RESPONSE: {
|
||||
// this->lastRequestTimestamp = -1;
|
||||
|
||||
// First four bytes mark the sequence id
|
||||
auto seqId = ntohl(extract<uint32_t>(packet.data, 0));
|
||||
if (seqId == (this->audioKeySequence - 1) &&
|
||||
audioKeyCallback != nullptr) {
|
||||
auto success = static_cast<RequestType>(packet.command) ==
|
||||
RequestType::AUDIO_KEY_SUCCESS_RESPONSE;
|
||||
audioKeyCallback(success, packet.data);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case RequestType::SEND:
|
||||
case RequestType::SUB:
|
||||
case RequestType::UNSUB: {
|
||||
CSPOT_LOG(debug, "Received mercury packet");
|
||||
|
||||
auto response = this->decodeResponse(packet.data);
|
||||
if (this->callbacks.count(response.sequenceId) > 0) {
|
||||
auto seqId = response.sequenceId;
|
||||
this->callbacks[response.sequenceId](response);
|
||||
this->callbacks.erase(this->callbacks.find(seqId));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case RequestType::SUBRES: {
|
||||
auto response = decodeResponse(packet.data);
|
||||
|
||||
auto uri = std::string(response.mercuryHeader.uri);
|
||||
if (this->subscriptions.count(uri) > 0) {
|
||||
this->subscriptions[uri](response);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void MercurySession::failAllPending() {
|
||||
Response response = { };
|
||||
response.fail = true;
|
||||
|
||||
// Fail all callbacks
|
||||
for (auto& it : this->callbacks) {
|
||||
it.second(response);
|
||||
}
|
||||
|
||||
// Fail all subscriptions
|
||||
for (auto& it : this->subscriptions) {
|
||||
it.second(response);
|
||||
}
|
||||
|
||||
// Remove references
|
||||
this->subscriptions = {};
|
||||
this->callbacks = {};
|
||||
}
|
||||
|
||||
MercurySession::Response MercurySession::decodeResponse(
|
||||
const std::vector<uint8_t>& data) {
|
||||
Response response = {};
|
||||
response.parts = {};
|
||||
|
||||
auto sequenceLength = ntohs(extract<uint16_t>(data, 0));
|
||||
response.sequenceId = hton64(extract<uint64_t>(data, 2));
|
||||
|
||||
auto partsNumber = ntohs(extract<uint16_t>(data, 11));
|
||||
|
||||
auto headerSize = ntohs(extract<uint16_t>(data, 13));
|
||||
auto headerBytes =
|
||||
std::vector<uint8_t>(data.begin() + 15, data.begin() + 15 + headerSize);
|
||||
|
||||
auto pos = 15 + headerSize;
|
||||
while (pos < data.size()) {
|
||||
auto partSize = ntohs(extract<uint16_t>(data, pos));
|
||||
|
||||
response.parts.push_back(std::vector<uint8_t>(
|
||||
data.begin() + pos + 2, data.begin() + pos + 2 + partSize));
|
||||
pos += 2 + partSize;
|
||||
}
|
||||
|
||||
pbDecode(response.mercuryHeader, Header_fields, headerBytes);
|
||||
response.fail = false;
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
uint64_t MercurySession::executeSubscription(RequestType method,
|
||||
const std::string& uri,
|
||||
ResponseCallback callback,
|
||||
ResponseCallback subscription,
|
||||
DataParts& payload) {
|
||||
CSPOT_LOG(debug, "Executing Mercury Request, type %s",
|
||||
RequestTypeMap[method].c_str());
|
||||
|
||||
// Encode header
|
||||
pbPutString(uri, tempMercuryHeader.uri);
|
||||
pbPutString(RequestTypeMap[method], tempMercuryHeader.method);
|
||||
|
||||
tempMercuryHeader.has_method = true;
|
||||
tempMercuryHeader.has_uri = true;
|
||||
|
||||
// GET and SEND are actually the same. Therefore the override
|
||||
// The difference between them is only in header's method
|
||||
if (method == RequestType::GET) {
|
||||
method = RequestType::SEND;
|
||||
}
|
||||
|
||||
if (method == RequestType::SUB) {
|
||||
this->subscriptions.insert({uri, subscription});
|
||||
}
|
||||
|
||||
auto headerBytes = pbEncode(Header_fields, &tempMercuryHeader);
|
||||
|
||||
this->callbacks.insert({sequenceId, callback});
|
||||
|
||||
// Structure: [Sequence size] [SequenceId] [0x1] [Payloads number]
|
||||
// [Header size] [Header] [Payloads (size + data)]
|
||||
|
||||
// Pack sequenceId
|
||||
auto sequenceIdBytes = pack<uint64_t>(hton64(this->sequenceId));
|
||||
auto sequenceSizeBytes = pack<uint16_t>(htons(sequenceIdBytes.size()));
|
||||
|
||||
sequenceIdBytes.insert(sequenceIdBytes.begin(), sequenceSizeBytes.begin(),
|
||||
sequenceSizeBytes.end());
|
||||
sequenceIdBytes.push_back(0x01);
|
||||
|
||||
auto payloadNum = pack<uint16_t>(htons(payload.size() + 1));
|
||||
sequenceIdBytes.insert(sequenceIdBytes.end(), payloadNum.begin(),
|
||||
payloadNum.end());
|
||||
|
||||
auto headerSizePayload = pack<uint16_t>(htons(headerBytes.size()));
|
||||
sequenceIdBytes.insert(sequenceIdBytes.end(), headerSizePayload.begin(),
|
||||
headerSizePayload.end());
|
||||
sequenceIdBytes.insert(sequenceIdBytes.end(), headerBytes.begin(),
|
||||
headerBytes.end());
|
||||
|
||||
// Encode all the payload parts
|
||||
for (int x = 0; x < payload.size(); x++) {
|
||||
headerSizePayload = pack<uint16_t>(htons(payload[x].size()));
|
||||
sequenceIdBytes.insert(sequenceIdBytes.end(), headerSizePayload.begin(),
|
||||
headerSizePayload.end());
|
||||
sequenceIdBytes.insert(sequenceIdBytes.end(), payload[x].begin(),
|
||||
payload[x].end());
|
||||
}
|
||||
|
||||
// Bump sequence id
|
||||
this->sequenceId += 1;
|
||||
|
||||
this->shanConn->sendPacket(
|
||||
static_cast<std::underlying_type<RequestType>::type>(method),
|
||||
sequenceIdBytes);
|
||||
|
||||
return this->sequenceId - 1;
|
||||
}
|
||||
|
||||
void MercurySession::requestAudioKey(const std::vector<uint8_t>& trackId,
|
||||
const std::vector<uint8_t>& fileId,
|
||||
AudioKeyCallback audioCallback) {
|
||||
auto buffer = fileId;
|
||||
this->audioKeyCallback = audioCallback;
|
||||
|
||||
// Structure: [FILEID] [TRACKID] [4 BYTES SEQUENCE ID] [0x00, 0x00]
|
||||
buffer.insert(buffer.end(), trackId.begin(), trackId.end());
|
||||
auto audioKeySequence = pack<uint32_t>(htonl(this->audioKeySequence));
|
||||
buffer.insert(buffer.end(), audioKeySequence.begin(), audioKeySequence.end());
|
||||
auto suffix = std::vector<uint8_t>({0x00, 0x00});
|
||||
buffer.insert(buffer.end(), suffix.begin(), suffix.end());
|
||||
|
||||
// Bump audio key sequence
|
||||
this->audioKeySequence += 1;
|
||||
|
||||
// Used for broken connection detection
|
||||
// this->lastRequestTimestamp = timeProvider->getSyncedTimestamp();
|
||||
this->shanConn->sendPacket(
|
||||
static_cast<uint8_t>(RequestType::AUDIO_KEY_REQUEST_COMMAND), buffer);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
#include "Packet.h"
|
||||
|
||||
Packet::Packet(uint8_t command, const std::vector<uint8_t> &data) {
|
||||
this->command = command;
|
||||
this->data = data;
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
#include "PlainConnection.h"
|
||||
#include <cstring>
|
||||
#ifdef _WIN32
|
||||
@@ -9,191 +8,188 @@
|
||||
#include <errno.h>
|
||||
#include "Logger.h"
|
||||
|
||||
static int getErrno()
|
||||
{
|
||||
using namespace cspot;
|
||||
|
||||
static int getErrno() {
|
||||
#ifdef _WIN32
|
||||
int code = WSAGetLastError();
|
||||
if (code == WSAETIMEDOUT) return ETIMEDOUT;
|
||||
if (code == WSAEINTR) return EINTR;
|
||||
return code;
|
||||
int code = WSAGetLastError();
|
||||
if (code == WSAETIMEDOUT)
|
||||
return ETIMEDOUT;
|
||||
if (code == WSAEINTR)
|
||||
return EINTR;
|
||||
return code;
|
||||
#else
|
||||
return errno;
|
||||
return errno;
|
||||
#endif
|
||||
}
|
||||
|
||||
PlainConnection::PlainConnection()
|
||||
{
|
||||
this->apSock = -1;
|
||||
PlainConnection::PlainConnection() {
|
||||
this->apSock = -1;
|
||||
};
|
||||
|
||||
PlainConnection::~PlainConnection()
|
||||
{
|
||||
closeSocket();
|
||||
PlainConnection::~PlainConnection() {
|
||||
this->close();
|
||||
};
|
||||
|
||||
void PlainConnection::connectToAp(std::string apAddress)
|
||||
{
|
||||
struct addrinfo h, *airoot, *ai;
|
||||
std::string hostname = apAddress.substr(0, apAddress.find(":"));
|
||||
std::string portStr = apAddress.substr(apAddress.find(":") + 1, apAddress.size());
|
||||
memset(&h, 0, sizeof(h));
|
||||
h.ai_family = AF_INET;
|
||||
h.ai_socktype = SOCK_STREAM;
|
||||
h.ai_protocol = IPPROTO_IP;
|
||||
void PlainConnection::connect(const std::string& apAddress) {
|
||||
struct addrinfo h, *airoot, *ai;
|
||||
std::string hostname = apAddress.substr(0, apAddress.find(":"));
|
||||
std::string portStr =
|
||||
apAddress.substr(apAddress.find(":") + 1, apAddress.size());
|
||||
memset(&h, 0, sizeof(h));
|
||||
h.ai_family = AF_INET;
|
||||
h.ai_socktype = SOCK_STREAM;
|
||||
h.ai_protocol = IPPROTO_IP;
|
||||
|
||||
// Lookup host
|
||||
if (getaddrinfo(hostname.c_str(), portStr.c_str(), &h, &airoot))
|
||||
{
|
||||
CSPOT_LOG(error, "getaddrinfo failed");
|
||||
}
|
||||
// Lookup host
|
||||
if (getaddrinfo(hostname.c_str(), portStr.c_str(), &h, &airoot)) {
|
||||
CSPOT_LOG(error, "getaddrinfo failed");
|
||||
}
|
||||
|
||||
// find the right ai, connect to server
|
||||
for (ai = airoot; ai; ai = ai->ai_next)
|
||||
{
|
||||
if (ai->ai_family != AF_INET && ai->ai_family != AF_INET6)
|
||||
continue;
|
||||
// find the right ai, connect to server
|
||||
for (ai = airoot; ai; ai = ai->ai_next) {
|
||||
if (ai->ai_family != AF_INET && ai->ai_family != AF_INET6)
|
||||
continue;
|
||||
|
||||
this->apSock = socket(ai->ai_family,
|
||||
ai->ai_socktype, ai->ai_protocol);
|
||||
if (this->apSock < 0)
|
||||
continue;
|
||||
this->apSock = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
|
||||
if (this->apSock < 0)
|
||||
continue;
|
||||
|
||||
if (connect(this->apSock,
|
||||
(struct sockaddr *)ai->ai_addr,
|
||||
ai->ai_addrlen) != -1)
|
||||
{
|
||||
if (::connect(this->apSock, (struct sockaddr*)ai->ai_addr, ai->ai_addrlen) !=
|
||||
-1) {
|
||||
#ifdef _WIN32
|
||||
uint32_t tv = 3000;
|
||||
uint32_t tv = 3000;
|
||||
#else
|
||||
struct timeval tv;
|
||||
tv.tv_sec = 3;
|
||||
tv.tv_usec = 0;
|
||||
struct timeval tv;
|
||||
tv.tv_sec = 3;
|
||||
tv.tv_usec = 0;
|
||||
#endif
|
||||
setsockopt(this->apSock, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof tv);
|
||||
setsockopt(this->apSock, SOL_SOCKET, SO_SNDTIMEO, (const char*)&tv, sizeof tv);
|
||||
setsockopt(this->apSock, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv,
|
||||
sizeof tv);
|
||||
setsockopt(this->apSock, SOL_SOCKET, SO_SNDTIMEO, (const char*)&tv,
|
||||
sizeof tv);
|
||||
|
||||
int flag = 1;
|
||||
setsockopt(this->apSock, /* socket affected */
|
||||
IPPROTO_TCP, /* set option at TCP level */
|
||||
TCP_NODELAY, /* name of option */
|
||||
(char *)&flag, /* the cast is historical cruft */
|
||||
sizeof(int)); /* length of option value */
|
||||
break;
|
||||
}
|
||||
|
||||
close(this->apSock);
|
||||
apSock = -1;
|
||||
throw std::runtime_error("Can't connect to spotify servers");
|
||||
int flag = 1;
|
||||
setsockopt(this->apSock, /* socket affected */
|
||||
IPPROTO_TCP, /* set option at TCP level */
|
||||
TCP_NODELAY, /* name of option */
|
||||
(char*)&flag, /* the cast is historical cruft */
|
||||
sizeof(int)); /* length of option value */
|
||||
break;
|
||||
}
|
||||
|
||||
freeaddrinfo(airoot);
|
||||
CSPOT_LOG(debug, "Connected to spotify server");
|
||||
#ifdef _WIN32
|
||||
closesocket(this->apSock);
|
||||
#else
|
||||
::close(this->apSock);
|
||||
#endif
|
||||
apSock = -1;
|
||||
throw std::runtime_error("Can't connect to spotify servers");
|
||||
}
|
||||
|
||||
freeaddrinfo(airoot);
|
||||
CSPOT_LOG(debug, "Connected to spotify server");
|
||||
}
|
||||
|
||||
std::vector<uint8_t> PlainConnection::recvPacket()
|
||||
{
|
||||
// Read packet size
|
||||
auto sizeData = readBlock(4);
|
||||
uint32_t packetSize = ntohl(extract<uint32_t>(sizeData, 0));
|
||||
// Read actual data
|
||||
auto data = readBlock(packetSize - 4);
|
||||
sizeData.insert(sizeData.end(), data.begin(), data.end());
|
||||
std::vector<uint8_t> PlainConnection::recvPacket() {
|
||||
// Read packet size
|
||||
std::vector<uint8_t> packetBuffer(4);
|
||||
readBlock(packetBuffer.data(), 4);
|
||||
uint32_t packetSize = ntohl(extract<uint32_t>(packetBuffer, 0));
|
||||
|
||||
return sizeData;
|
||||
packetBuffer.resize(packetSize, 0);
|
||||
|
||||
// Read actual data
|
||||
readBlock(packetBuffer.data() + 4, packetSize - 4);
|
||||
|
||||
return packetBuffer;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> PlainConnection::sendPrefixPacket(const std::vector<uint8_t> &prefix, const std::vector<uint8_t> &data)
|
||||
{
|
||||
// Calculate full packet length
|
||||
uint32_t actualSize = prefix.size() + data.size() + sizeof(uint32_t);
|
||||
std::vector<uint8_t> PlainConnection::sendPrefixPacket(
|
||||
const std::vector<uint8_t>& prefix, const std::vector<uint8_t>& data) {
|
||||
// Calculate full packet length
|
||||
uint32_t actualSize = prefix.size() + data.size() + sizeof(uint32_t);
|
||||
|
||||
// Packet structure [PREFIX] + [SIZE] + [DATA]
|
||||
auto sizeRaw = pack<uint32_t>(htonl(actualSize));
|
||||
sizeRaw.insert(sizeRaw.begin(), prefix.begin(), prefix.end());
|
||||
sizeRaw.insert(sizeRaw.end(), data.begin(), data.end());
|
||||
// Packet structure [PREFIX] + [SIZE] + [DATA]
|
||||
auto sizeRaw = pack<uint32_t>(htonl(actualSize));
|
||||
sizeRaw.insert(sizeRaw.begin(), prefix.begin(), prefix.end());
|
||||
sizeRaw.insert(sizeRaw.end(), data.begin(), data.end());
|
||||
|
||||
// Actually write it to the server
|
||||
writeBlock(sizeRaw);
|
||||
// Actually write it to the server
|
||||
writeBlock(sizeRaw);
|
||||
|
||||
return sizeRaw;
|
||||
return sizeRaw;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> PlainConnection::readBlock(size_t size)
|
||||
{
|
||||
std::vector<uint8_t> buf(size);
|
||||
unsigned int idx = 0;
|
||||
ssize_t n;
|
||||
int retries = 0;
|
||||
// printf("START READ\n");
|
||||
void PlainConnection::readBlock(const uint8_t* dst, size_t size) {
|
||||
unsigned int idx = 0;
|
||||
ssize_t n;
|
||||
int retries = 0;
|
||||
|
||||
while (idx < size)
|
||||
{
|
||||
READ:
|
||||
if ((n = recv(this->apSock, (char*) &buf[idx], size - idx, 0)) <= 0)
|
||||
{
|
||||
switch (getErrno())
|
||||
{
|
||||
case EAGAIN:
|
||||
case ETIMEDOUT:
|
||||
if (timeoutHandler())
|
||||
{
|
||||
CSPOT_LOG(error, "Connection lost, will need to reconnect...");
|
||||
throw std::runtime_error("Reconnection required");
|
||||
}
|
||||
goto READ;
|
||||
case EINTR:
|
||||
break;
|
||||
default:
|
||||
if (retries++ > 4) throw std::runtime_error("Error in read");
|
||||
goto READ;
|
||||
}
|
||||
}
|
||||
idx += n;
|
||||
while (idx < size) {
|
||||
READ:
|
||||
if ((n = recv(this->apSock, (char*)&dst[idx], size - idx, 0)) <= 0) {
|
||||
switch (getErrno()) {
|
||||
case EAGAIN:
|
||||
case ETIMEDOUT:
|
||||
// if (timeoutHandler()) {
|
||||
// CSPOT_LOG(error, "Connection lost, will need to reconnect...");
|
||||
// throw std::runtime_error("Reconnection required");
|
||||
// }
|
||||
goto READ;
|
||||
case EINTR:
|
||||
break;
|
||||
default:
|
||||
if (retries++ > 4)
|
||||
throw std::runtime_error("Error in read");
|
||||
goto READ;
|
||||
}
|
||||
}
|
||||
// printf("FINISH READ\n");
|
||||
return buf;
|
||||
idx += n;
|
||||
}
|
||||
}
|
||||
|
||||
size_t PlainConnection::writeBlock(const std::vector<uint8_t> &data)
|
||||
{
|
||||
unsigned int idx = 0;
|
||||
ssize_t n;
|
||||
// printf("START WRITE\n");
|
||||
int retries = 0;
|
||||
size_t PlainConnection::writeBlock(const std::vector<uint8_t>& data) {
|
||||
unsigned int idx = 0;
|
||||
ssize_t n;
|
||||
|
||||
while (idx < data.size())
|
||||
{
|
||||
WRITE:
|
||||
if ((n = send(this->apSock, (char*) &data[idx], data.size() - idx < 64 ? data.size() - idx : 64, 0)) <= 0)
|
||||
{
|
||||
switch (getErrno())
|
||||
{
|
||||
case EAGAIN:
|
||||
case ETIMEDOUT:
|
||||
if (timeoutHandler())
|
||||
{
|
||||
throw std::runtime_error("Reconnection required");
|
||||
}
|
||||
goto WRITE;
|
||||
case EINTR:
|
||||
break;
|
||||
default:
|
||||
if (retries++ > 4) throw std::runtime_error("Error in write");
|
||||
goto WRITE;
|
||||
}
|
||||
}
|
||||
idx += n;
|
||||
int retries = 0;
|
||||
|
||||
while (idx < data.size()) {
|
||||
WRITE:
|
||||
if ((n = send(this->apSock, (char*)&data[idx],
|
||||
data.size() - idx < 64 ? data.size() - idx : 64, 0)) <= 0) {
|
||||
switch (getErrno()) {
|
||||
case EAGAIN:
|
||||
case ETIMEDOUT:
|
||||
// if (timeoutHandler()) {
|
||||
// throw std::runtime_error("Reconnection required");
|
||||
// }
|
||||
goto WRITE;
|
||||
case EINTR:
|
||||
break;
|
||||
default:
|
||||
if (retries++ > 4)
|
||||
throw std::runtime_error("Error in write");
|
||||
goto WRITE;
|
||||
}
|
||||
}
|
||||
idx += n;
|
||||
}
|
||||
|
||||
return data.size();
|
||||
return data.size();
|
||||
}
|
||||
|
||||
void PlainConnection::closeSocket()
|
||||
{
|
||||
if (this->apSock < 0) return;
|
||||
void PlainConnection::close() {
|
||||
if (this->apSock < 0)
|
||||
return;
|
||||
|
||||
CSPOT_LOG(info, "Closing socket...");
|
||||
shutdown(this->apSock, SHUT_RDWR);
|
||||
close(this->apSock);
|
||||
this->apSock = -1;
|
||||
CSPOT_LOG(info, "Closing socket...");
|
||||
shutdown(this->apSock, SHUT_RDWR);
|
||||
#ifdef _WIN32
|
||||
closesocket(this->apSock);
|
||||
#else
|
||||
::close(this->apSock);
|
||||
#endif
|
||||
this->apSock = -1;
|
||||
}
|
||||
|
||||
299
components/spotify/cspot/src/PlaybackState.cpp
Normal file
299
components/spotify/cspot/src/PlaybackState.cpp
Normal file
@@ -0,0 +1,299 @@
|
||||
#include "PlaybackState.h"
|
||||
#include <memory>
|
||||
#include "CSpotContext.h"
|
||||
#include "Logger.h"
|
||||
|
||||
using namespace cspot;
|
||||
|
||||
PlaybackState::PlaybackState(std::shared_ptr<cspot::Context> ctx) {
|
||||
this->ctx = ctx;
|
||||
innerFrame = {};
|
||||
remoteFrame = {};
|
||||
|
||||
// Prepare default state
|
||||
innerFrame.state.has_position_ms = true;
|
||||
innerFrame.state.position_ms = 0;
|
||||
|
||||
innerFrame.state.status = PlayStatus_kPlayStatusStop;
|
||||
innerFrame.state.has_status = true;
|
||||
|
||||
innerFrame.state.position_measured_at = 0;
|
||||
innerFrame.state.has_position_measured_at = true;
|
||||
|
||||
innerFrame.state.shuffle = false;
|
||||
innerFrame.state.has_shuffle = true;
|
||||
|
||||
innerFrame.state.repeat = false;
|
||||
innerFrame.state.has_repeat = true;
|
||||
|
||||
innerFrame.device_state.sw_version = strdup(swVersion);
|
||||
|
||||
innerFrame.device_state.is_active = false;
|
||||
innerFrame.device_state.has_is_active = true;
|
||||
|
||||
innerFrame.device_state.can_play = true;
|
||||
innerFrame.device_state.has_can_play = true;
|
||||
|
||||
innerFrame.device_state.volume = ctx->config.volume;
|
||||
innerFrame.device_state.has_volume = true;
|
||||
|
||||
innerFrame.device_state.name = strdup(ctx->config.deviceName.c_str());
|
||||
|
||||
innerFrame.state.track_count = 0;
|
||||
|
||||
// Prepare player's capabilities
|
||||
addCapability(CapabilityType_kCanBePlayer, 1);
|
||||
addCapability(CapabilityType_kDeviceType, 4);
|
||||
addCapability(CapabilityType_kGaiaEqConnectId, 1);
|
||||
addCapability(CapabilityType_kSupportsLogout, 0);
|
||||
addCapability(CapabilityType_kIsObservable, 1);
|
||||
addCapability(CapabilityType_kVolumeSteps, 64);
|
||||
addCapability(CapabilityType_kSupportedContexts, -1,
|
||||
std::vector<std::string>({"album", "playlist", "search",
|
||||
"inbox", "toplist", "starred",
|
||||
"publishedstarred", "track"}));
|
||||
addCapability(CapabilityType_kSupportedTypes, -1,
|
||||
std::vector<std::string>({"audio/local", "audio/track",
|
||||
"audio/episode", "local", "track"}));
|
||||
innerFrame.device_state.capabilities_count = 8;
|
||||
}
|
||||
|
||||
PlaybackState::~PlaybackState() {
|
||||
pb_release(Frame_fields, &innerFrame);
|
||||
pb_release(Frame_fields, &remoteFrame);
|
||||
}
|
||||
|
||||
void PlaybackState::setPlaybackState(const PlaybackState::State state) {
|
||||
switch (state) {
|
||||
case State::Loading:
|
||||
// Prepare the playback at position 0
|
||||
innerFrame.state.status = PlayStatus_kPlayStatusPause;
|
||||
innerFrame.state.position_ms = 0;
|
||||
innerFrame.state.position_measured_at =
|
||||
ctx->timeProvider->getSyncedTimestamp();
|
||||
break;
|
||||
case State::Playing:
|
||||
innerFrame.state.status = PlayStatus_kPlayStatusPlay;
|
||||
innerFrame.state.position_measured_at =
|
||||
ctx->timeProvider->getSyncedTimestamp();
|
||||
break;
|
||||
case State::Stopped:
|
||||
break;
|
||||
case State::Paused:
|
||||
// Update state and recalculate current song position
|
||||
innerFrame.state.status = PlayStatus_kPlayStatusPause;
|
||||
uint32_t diff = ctx->timeProvider->getSyncedTimestamp() -
|
||||
innerFrame.state.position_measured_at;
|
||||
this->updatePositionMs(innerFrame.state.position_ms + diff);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool PlaybackState::isActive() {
|
||||
return innerFrame.device_state.is_active;
|
||||
}
|
||||
|
||||
bool PlaybackState::nextTrack() {
|
||||
innerFrame.state.playing_track_index++;
|
||||
|
||||
if (innerFrame.state.playing_track_index >= innerFrame.state.track_count) {
|
||||
|
||||
innerFrame.state.playing_track_index = 0;
|
||||
|
||||
if (!innerFrame.state.repeat) {
|
||||
setPlaybackState(State::Paused);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void PlaybackState::prevTrack() {
|
||||
if (innerFrame.state.playing_track_index > 0) {
|
||||
innerFrame.state.playing_track_index--;
|
||||
} else if (innerFrame.state.repeat) {
|
||||
innerFrame.state.playing_track_index = innerFrame.state.track_count - 1;
|
||||
}
|
||||
}
|
||||
|
||||
void PlaybackState::setActive(bool isActive) {
|
||||
innerFrame.device_state.is_active = isActive;
|
||||
if (isActive) {
|
||||
innerFrame.device_state.became_active_at =
|
||||
ctx->timeProvider->getSyncedTimestamp();
|
||||
innerFrame.device_state.has_became_active_at = true;
|
||||
}
|
||||
}
|
||||
|
||||
void PlaybackState::updatePositionMs(uint32_t position) {
|
||||
innerFrame.state.position_ms = position;
|
||||
innerFrame.state.position_measured_at =
|
||||
ctx->timeProvider->getSyncedTimestamp();
|
||||
}
|
||||
|
||||
#define FREE(ptr) \
|
||||
{ \
|
||||
free(ptr); \
|
||||
ptr = NULL; \
|
||||
}
|
||||
#define STRDUP(dst, src) \
|
||||
if (src != NULL) { \
|
||||
dst = strdup(src); \
|
||||
} else { \
|
||||
FREE(dst); \
|
||||
} // strdup null pointer safe
|
||||
|
||||
void PlaybackState::updateTracks() {
|
||||
CSPOT_LOG(info, "---- Track count %d", remoteFrame.state.track_count);
|
||||
CSPOT_LOG(info, "---- Inner track count %d", innerFrame.state.track_count);
|
||||
CSPOT_LOG(info, "--- Context URI %s", remoteFrame.state.context_uri);
|
||||
|
||||
// free unused tracks
|
||||
if (innerFrame.state.track_count > remoteFrame.state.track_count) {
|
||||
for (uint16_t i = remoteFrame.state.track_count;
|
||||
i < innerFrame.state.track_count; ++i) {
|
||||
FREE(innerFrame.state.track[i].gid);
|
||||
FREE(innerFrame.state.track[i].uri);
|
||||
FREE(innerFrame.state.track[i].context);
|
||||
}
|
||||
}
|
||||
|
||||
// reallocate memory for new tracks
|
||||
innerFrame.state.track = (TrackRef*)realloc(
|
||||
innerFrame.state.track, sizeof(TrackRef) * remoteFrame.state.track_count);
|
||||
|
||||
for (uint16_t i = 0; i < remoteFrame.state.track_count; ++i) {
|
||||
if (i >= innerFrame.state.track_count) {
|
||||
innerFrame.state.track[i].gid = NULL;
|
||||
innerFrame.state.track[i].uri = NULL;
|
||||
innerFrame.state.track[i].context = NULL;
|
||||
}
|
||||
|
||||
if (remoteFrame.state.track[i].gid != NULL) {
|
||||
uint16_t gid_size = remoteFrame.state.track[i].gid->size;
|
||||
innerFrame.state.track[i].gid = (pb_bytes_array_t*)realloc(
|
||||
innerFrame.state.track[i].gid, PB_BYTES_ARRAY_T_ALLOCSIZE(gid_size));
|
||||
|
||||
memcpy(innerFrame.state.track[i].gid->bytes,
|
||||
remoteFrame.state.track[i].gid->bytes, gid_size);
|
||||
innerFrame.state.track[i].gid->size = gid_size;
|
||||
}
|
||||
innerFrame.state.track[i].has_queued =
|
||||
remoteFrame.state.track[i].has_queued;
|
||||
innerFrame.state.track[i].queued = remoteFrame.state.track[i].queued;
|
||||
|
||||
STRDUP(innerFrame.state.track[i].uri, remoteFrame.state.track[i].uri);
|
||||
STRDUP(innerFrame.state.track[i].context,
|
||||
remoteFrame.state.track[i].context);
|
||||
}
|
||||
|
||||
innerFrame.state.context_uri = (char*)realloc(
|
||||
innerFrame.state.context_uri, strlen(remoteFrame.state.context_uri) + 1);
|
||||
strcpy(innerFrame.state.context_uri, remoteFrame.state.context_uri);
|
||||
|
||||
innerFrame.state.track_count = remoteFrame.state.track_count;
|
||||
innerFrame.state.has_playing_track_index = true;
|
||||
innerFrame.state.playing_track_index = remoteFrame.state.playing_track_index;
|
||||
|
||||
if (remoteFrame.state.repeat) {
|
||||
setRepeat(true);
|
||||
}
|
||||
|
||||
if (remoteFrame.state.shuffle) {
|
||||
setShuffle(true);
|
||||
}
|
||||
}
|
||||
|
||||
void PlaybackState::setVolume(uint32_t volume) {
|
||||
innerFrame.device_state.volume = volume;
|
||||
ctx->config.volume = volume;
|
||||
}
|
||||
|
||||
void PlaybackState::setShuffle(bool shuffle) {
|
||||
innerFrame.state.shuffle = shuffle;
|
||||
if (shuffle) {
|
||||
// Put current song at the begining
|
||||
std::swap(innerFrame.state.track[0],
|
||||
innerFrame.state.track[innerFrame.state.playing_track_index]);
|
||||
|
||||
// Shuffle current tracks
|
||||
for (int x = 1; x < innerFrame.state.track_count - 1; x++) {
|
||||
auto j = x + (std::rand() % (innerFrame.state.track_count - x));
|
||||
std::swap(innerFrame.state.track[j], innerFrame.state.track[x]);
|
||||
}
|
||||
innerFrame.state.playing_track_index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void PlaybackState::setRepeat(bool repeat) {
|
||||
innerFrame.state.repeat = repeat;
|
||||
}
|
||||
|
||||
TrackRef* PlaybackState::getCurrentTrackRef() {
|
||||
if (innerFrame.state.playing_track_index >= innerFrame.state.track_count) {
|
||||
return nullptr;
|
||||
}
|
||||
return &innerFrame.state.track[innerFrame.state.playing_track_index];
|
||||
}
|
||||
|
||||
TrackRef* PlaybackState::getNextTrackRef() {
|
||||
if ((innerFrame.state.playing_track_index + 1) >= innerFrame.state.track_count) {
|
||||
if (innerFrame.state.repeat) {
|
||||
return &innerFrame.state.track[0];
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return &innerFrame.state.track[innerFrame.state.playing_track_index + 1];
|
||||
}
|
||||
|
||||
std::vector<uint8_t> PlaybackState::encodeCurrentFrame(MessageType typ) {
|
||||
free(innerFrame.ident);
|
||||
free(innerFrame.protocol_version);
|
||||
|
||||
// Prepare current frame info
|
||||
innerFrame.version = 1;
|
||||
innerFrame.ident = strdup(ctx->config.deviceId.c_str());
|
||||
innerFrame.seq_nr = this->seqNum;
|
||||
innerFrame.protocol_version = strdup(protocolVersion);
|
||||
innerFrame.typ = typ;
|
||||
innerFrame.state_update_id = ctx->timeProvider->getSyncedTimestamp();
|
||||
innerFrame.has_version = true;
|
||||
innerFrame.has_seq_nr = true;
|
||||
innerFrame.recipient_count = 0;
|
||||
innerFrame.has_state = true;
|
||||
innerFrame.has_device_state = true;
|
||||
innerFrame.has_typ = true;
|
||||
innerFrame.has_state_update_id = true;
|
||||
|
||||
this->seqNum += 1;
|
||||
return pbEncode(Frame_fields, &innerFrame);
|
||||
}
|
||||
|
||||
// Wraps messy nanopb setters. @TODO: find a better way to handle this
|
||||
void PlaybackState::addCapability(CapabilityType typ, int intValue,
|
||||
std::vector<std::string> stringValue) {
|
||||
innerFrame.device_state.capabilities[capabilityIndex].has_typ = true;
|
||||
this->innerFrame.device_state.capabilities[capabilityIndex].typ = typ;
|
||||
|
||||
if (intValue != -1) {
|
||||
this->innerFrame.device_state.capabilities[capabilityIndex].intValue[0] =
|
||||
intValue;
|
||||
this->innerFrame.device_state.capabilities[capabilityIndex].intValue_count =
|
||||
1;
|
||||
} else {
|
||||
this->innerFrame.device_state.capabilities[capabilityIndex].intValue_count =
|
||||
0;
|
||||
}
|
||||
|
||||
for (int x = 0; x < stringValue.size(); x++) {
|
||||
pbPutString(stringValue[x],
|
||||
this->innerFrame.device_state.capabilities[capabilityIndex]
|
||||
.stringValue[x]);
|
||||
}
|
||||
|
||||
this->innerFrame.device_state.capabilities[capabilityIndex]
|
||||
.stringValue_count = stringValue.size();
|
||||
this->capabilityIndex += 1;
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
#include "Player.h"
|
||||
#include "Logger.h"
|
||||
|
||||
// #include <valgrind/memcheck.h>
|
||||
|
||||
Player::Player(std::shared_ptr<MercuryManager> manager, std::shared_ptr<AudioSink> audioSink): bell::Task("player", 10 * 1024, -2, 1)
|
||||
{
|
||||
this->audioSink = audioSink;
|
||||
this->manager = manager;
|
||||
startTask();
|
||||
}
|
||||
|
||||
void Player::pause()
|
||||
{
|
||||
if (currentTrack != nullptr)
|
||||
{
|
||||
if (currentTrack->audioStream != nullptr)
|
||||
{
|
||||
this->currentTrack->audioStream->isPaused = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Player::play()
|
||||
{
|
||||
if (currentTrack != nullptr)
|
||||
{
|
||||
if (currentTrack->audioStream != nullptr)
|
||||
{
|
||||
this->currentTrack->audioStream->isPaused = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Player::setVolume(uint32_t volume)
|
||||
{
|
||||
this->volume = (volume / (double)MAX_VOLUME) * 255;
|
||||
|
||||
// Calculate and cache log volume value
|
||||
auto vol = 255 - this->volume;
|
||||
uint32_t value = (log10(255 / ((float)vol + 1)) * 105.54571334);
|
||||
if (value >= 254) value = 256;
|
||||
logVolume = value << 8; // *256
|
||||
|
||||
// Pass volume event to the sink if volume is sink-handled
|
||||
if (!this->audioSink->softwareVolumeControl)
|
||||
{
|
||||
this->audioSink->volumeChanged(volume);
|
||||
}
|
||||
}
|
||||
|
||||
void Player::seekMs(size_t positionMs)
|
||||
{
|
||||
if (currentTrack != nullptr)
|
||||
{
|
||||
if (currentTrack->audioStream != nullptr)
|
||||
{
|
||||
this->currentTrack->audioStream->seekMs(positionMs);
|
||||
}
|
||||
}
|
||||
// VALGRIND_DO_LEAK_CHECK;
|
||||
}
|
||||
|
||||
void Player::feedPCM(uint8_t *data, size_t len)
|
||||
{
|
||||
// Simple digital volume control alg
|
||||
// @TODO actually extract it somewhere
|
||||
if (this->audioSink->softwareVolumeControl)
|
||||
{
|
||||
int16_t* psample;
|
||||
int32_t temp;
|
||||
psample = (int16_t*)(data);
|
||||
size_t half_len = len / 2;
|
||||
for (uint32_t i = 0; i < half_len; i++)
|
||||
{
|
||||
// Offset data for unsigned sinks
|
||||
if (this->audioSink->usign)
|
||||
{
|
||||
temp = ((int32_t)psample[i] + 0x8000) * logVolume;
|
||||
}
|
||||
else
|
||||
{
|
||||
temp = ((int32_t)psample[i]) * logVolume;
|
||||
}
|
||||
psample[i] = (temp >> 16) & 0xFFFF;
|
||||
}
|
||||
}
|
||||
|
||||
this->audioSink->feedPCMFrames(data, len);
|
||||
}
|
||||
|
||||
void Player::runTask()
|
||||
{
|
||||
uint8_t *pcmOut = (uint8_t *) malloc(4096 / 4);
|
||||
std::scoped_lock lock(this->runningMutex);
|
||||
this->isRunning = true;
|
||||
while (isRunning)
|
||||
{
|
||||
if(nextTrack != nullptr && nextTrack->loaded)
|
||||
{
|
||||
this->nextTrackMutex.lock();
|
||||
currentTrack = this->nextTrack;
|
||||
this->nextTrack = nullptr;
|
||||
this->nextTrackMutex.unlock();
|
||||
|
||||
currentTrack->audioStream->startPlaybackLoop(pcmOut, 4096 / 4);
|
||||
currentTrack->loadedTrackCallback = nullptr;
|
||||
currentTrack->audioStream->streamFinishedCallback = nullptr;
|
||||
currentTrack->audioStream->audioSink = nullptr;
|
||||
currentTrack->audioStream->pcmCallback = nullptr;
|
||||
|
||||
delete currentTrack;
|
||||
currentTrack = nullptr;
|
||||
}
|
||||
else
|
||||
{
|
||||
usleep(20000);
|
||||
}
|
||||
|
||||
}
|
||||
free(pcmOut);
|
||||
}
|
||||
|
||||
void Player::stop() {
|
||||
CSPOT_LOG(info, "Trying to stop");
|
||||
this->isRunning = false;
|
||||
cancelCurrentTrack();
|
||||
std::scoped_lock lock(this->runningMutex);
|
||||
if(this->nextTrack != nullptr)
|
||||
{
|
||||
delete this->nextTrack;
|
||||
}
|
||||
this->isRunning = false;
|
||||
CSPOT_LOG(info, "Track cancelled");
|
||||
cancelCurrentTrack();
|
||||
CSPOT_LOG(info, "Stopping player");
|
||||
}
|
||||
|
||||
void Player::cancelCurrentTrack()
|
||||
{
|
||||
if (currentTrack != nullptr)
|
||||
{
|
||||
if (currentTrack->audioStream != nullptr && currentTrack->audioStream->isRunning)
|
||||
{
|
||||
currentTrack->audioStream->isRunning = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Player::handleLoad(std::shared_ptr<TrackReference> trackReference, std::function<void(bool)>& trackLoadedCallback, uint32_t position_ms, bool isPaused)
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(loadTrackMutex);
|
||||
|
||||
pcmDataCallback framesCallback = [=](uint8_t *frames, size_t len) {
|
||||
this->feedPCM(frames, len);
|
||||
};
|
||||
|
||||
this->nextTrackMutex.lock();
|
||||
if(this->nextTrack != nullptr)
|
||||
{
|
||||
delete this->nextTrack;
|
||||
this->nextTrack = nullptr;
|
||||
}
|
||||
|
||||
this->nextTrack = new SpotifyTrack(this->manager, trackReference, position_ms, isPaused);
|
||||
|
||||
this->nextTrack->trackInfoReceived = this->trackChanged;
|
||||
this->nextTrack->loadedTrackCallback = [this, framesCallback, trackLoadedCallback]() {
|
||||
bool needFlush = currentTrack != nullptr && currentTrack->audioStream != nullptr && currentTrack->audioStream->isRunning;
|
||||
cancelCurrentTrack();
|
||||
trackLoadedCallback(needFlush);
|
||||
|
||||
this->nextTrackMutex.lock();
|
||||
this->nextTrack->audioStream->streamFinishedCallback = this->endOfFileCallback;
|
||||
this->nextTrack->audioStream->audioSink = this->audioSink;
|
||||
this->nextTrack->audioStream->pcmCallback = framesCallback;
|
||||
this->nextTrack->loaded = true;
|
||||
this->nextTrackMutex.unlock();
|
||||
|
||||
};
|
||||
this->nextTrackMutex.unlock();
|
||||
}
|
||||
@@ -1,286 +0,0 @@
|
||||
#include "PlayerState.h"
|
||||
#include "Logger.h"
|
||||
#include "ConfigJSON.h"
|
||||
|
||||
PlayerState::PlayerState(std::shared_ptr<TimeProvider> timeProvider)
|
||||
{
|
||||
this->timeProvider = timeProvider;
|
||||
innerFrame = {};
|
||||
remoteFrame = {};
|
||||
|
||||
// Prepare default state
|
||||
innerFrame.state.has_position_ms = true;
|
||||
innerFrame.state.position_ms = 0;
|
||||
|
||||
innerFrame.state.status = PlayStatus_kPlayStatusStop;
|
||||
innerFrame.state.has_status = true;
|
||||
|
||||
innerFrame.state.position_measured_at = 0;
|
||||
innerFrame.state.has_position_measured_at = true;
|
||||
|
||||
innerFrame.state.shuffle = false;
|
||||
innerFrame.state.has_shuffle = true;
|
||||
|
||||
innerFrame.state.repeat = false;
|
||||
innerFrame.state.has_repeat = true;
|
||||
|
||||
innerFrame.device_state.sw_version = strdup(swVersion);
|
||||
|
||||
innerFrame.device_state.is_active = false;
|
||||
innerFrame.device_state.has_is_active = true;
|
||||
|
||||
innerFrame.device_state.can_play = true;
|
||||
innerFrame.device_state.has_can_play = true;
|
||||
|
||||
innerFrame.device_state.volume = configMan->volume;
|
||||
innerFrame.device_state.has_volume = true;
|
||||
|
||||
innerFrame.device_state.name = strdup(configMan->deviceName.c_str());
|
||||
|
||||
innerFrame.state.track_count = 0;
|
||||
|
||||
// Prepare player's capabilities
|
||||
addCapability(CapabilityType_kCanBePlayer, 1);
|
||||
addCapability(CapabilityType_kDeviceType, 4);
|
||||
addCapability(CapabilityType_kGaiaEqConnectId, 1);
|
||||
addCapability(CapabilityType_kSupportsLogout, 0);
|
||||
addCapability(CapabilityType_kIsObservable, 1);
|
||||
addCapability(CapabilityType_kVolumeSteps, 64);
|
||||
addCapability(CapabilityType_kSupportedContexts, -1,
|
||||
std::vector<std::string>({"album", "playlist", "search", "inbox",
|
||||
"toplist", "starred", "publishedstarred", "track"}));
|
||||
addCapability(CapabilityType_kSupportedTypes, -1,
|
||||
std::vector<std::string>({"audio/local", "audio/track", "audio/episode", "local", "track"}));
|
||||
innerFrame.device_state.capabilities_count = 8;
|
||||
}
|
||||
|
||||
PlayerState::~PlayerState() {
|
||||
pb_release(Frame_fields, &innerFrame);
|
||||
pb_release(Frame_fields, &remoteFrame);
|
||||
}
|
||||
|
||||
void PlayerState::setPlaybackState(const PlaybackState state)
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case PlaybackState::Loading:
|
||||
// Prepare the playback at position 0
|
||||
innerFrame.state.status = PlayStatus_kPlayStatusPause;
|
||||
innerFrame.state.position_ms = 0;
|
||||
innerFrame.state.position_measured_at = timeProvider->getSyncedTimestamp();
|
||||
break;
|
||||
case PlaybackState::Playing:
|
||||
innerFrame.state.status = PlayStatus_kPlayStatusPlay;
|
||||
innerFrame.state.position_measured_at = timeProvider->getSyncedTimestamp();
|
||||
break;
|
||||
case PlaybackState::Stopped:
|
||||
break;
|
||||
case PlaybackState::Paused:
|
||||
// Update state and recalculate current song position
|
||||
innerFrame.state.status = PlayStatus_kPlayStatusPause;
|
||||
uint32_t diff = timeProvider->getSyncedTimestamp() - innerFrame.state.position_measured_at;
|
||||
this->updatePositionMs(innerFrame.state.position_ms + diff);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool PlayerState::isActive()
|
||||
{
|
||||
return innerFrame.device_state.is_active;
|
||||
}
|
||||
|
||||
bool PlayerState::nextTrack()
|
||||
{
|
||||
if (innerFrame.state.repeat) return true;
|
||||
|
||||
innerFrame.state.playing_track_index++;
|
||||
|
||||
if (innerFrame.state.playing_track_index >= innerFrame.state.track_count)
|
||||
{
|
||||
innerFrame.state.playing_track_index = 0;
|
||||
if (!innerFrame.state.repeat)
|
||||
{
|
||||
setPlaybackState(PlaybackState::Paused);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void PlayerState::prevTrack()
|
||||
{
|
||||
if (innerFrame.state.playing_track_index > 0)
|
||||
{
|
||||
innerFrame.state.playing_track_index--;
|
||||
}
|
||||
else if (innerFrame.state.repeat)
|
||||
{
|
||||
innerFrame.state.playing_track_index = innerFrame.state.track_count - 1;
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerState::setActive(bool isActive)
|
||||
{
|
||||
innerFrame.device_state.is_active = isActive;
|
||||
if (isActive)
|
||||
{
|
||||
innerFrame.device_state.became_active_at = timeProvider->getSyncedTimestamp();
|
||||
innerFrame.device_state.has_became_active_at = true;
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerState::updatePositionMs(uint32_t position)
|
||||
{
|
||||
innerFrame.state.position_ms = position;
|
||||
innerFrame.state.position_measured_at = timeProvider->getSyncedTimestamp();
|
||||
}
|
||||
|
||||
#define FREE(ptr) { free(ptr); ptr = NULL; }
|
||||
#define STRDUP(dst, src) if(src != NULL) { dst = strdup(src); } else { FREE(dst); } // strdup null pointer safe
|
||||
|
||||
void PlayerState::updateTracks()
|
||||
{
|
||||
CSPOT_LOG(info, "---- Track count %d", remoteFrame.state.track_count);
|
||||
CSPOT_LOG(info, "---- Inner track count %d", innerFrame.state.track_count);
|
||||
|
||||
// free unused tracks
|
||||
if(innerFrame.state.track_count > remoteFrame.state.track_count)
|
||||
{
|
||||
for(uint16_t i = remoteFrame.state.track_count; i < innerFrame.state.track_count; ++i)
|
||||
{
|
||||
FREE(innerFrame.state.track[i].gid);
|
||||
FREE(innerFrame.state.track[i].uri);
|
||||
FREE(innerFrame.state.track[i].context);
|
||||
}
|
||||
}
|
||||
|
||||
// reallocate memory for new tracks
|
||||
innerFrame.state.track = (TrackRef *) realloc(innerFrame.state.track, sizeof(TrackRef) * remoteFrame.state.track_count);
|
||||
|
||||
for(uint16_t i = 0; i < remoteFrame.state.track_count; ++i)
|
||||
{
|
||||
if(i >= innerFrame.state.track_count) {
|
||||
innerFrame.state.track[i].gid = NULL;
|
||||
innerFrame.state.track[i].uri = NULL;
|
||||
innerFrame.state.track[i].context = NULL;
|
||||
}
|
||||
|
||||
if(remoteFrame.state.track[i].gid != NULL)
|
||||
{
|
||||
uint16_t gid_size = remoteFrame.state.track[i].gid->size;
|
||||
innerFrame.state.track[i].gid = (pb_bytes_array_t *) realloc(innerFrame.state.track[i].gid, PB_BYTES_ARRAY_T_ALLOCSIZE(gid_size));
|
||||
|
||||
memcpy(innerFrame.state.track[i].gid->bytes, remoteFrame.state.track[i].gid->bytes, gid_size);
|
||||
innerFrame.state.track[i].gid->size = gid_size;
|
||||
}
|
||||
innerFrame.state.track[i].has_queued = remoteFrame.state.track[i].has_queued;
|
||||
innerFrame.state.track[i].queued = remoteFrame.state.track[i].queued;
|
||||
|
||||
STRDUP(innerFrame.state.track[i].uri, remoteFrame.state.track[i].uri);
|
||||
STRDUP(innerFrame.state.track[i].context, remoteFrame.state.track[i].context);
|
||||
}
|
||||
|
||||
innerFrame.state.context_uri = (char *) realloc(innerFrame.state.context_uri,
|
||||
strlen(remoteFrame.state.context_uri) + 1);
|
||||
strcpy(innerFrame.state.context_uri, remoteFrame.state.context_uri);
|
||||
|
||||
innerFrame.state.track_count = remoteFrame.state.track_count;
|
||||
innerFrame.state.has_playing_track_index = true;
|
||||
innerFrame.state.playing_track_index = remoteFrame.state.playing_track_index;
|
||||
|
||||
if (remoteFrame.state.repeat)
|
||||
{
|
||||
setRepeat(true);
|
||||
}
|
||||
|
||||
if (remoteFrame.state.shuffle)
|
||||
{
|
||||
setShuffle(true);
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerState::setVolume(uint32_t volume)
|
||||
{
|
||||
innerFrame.device_state.volume = volume;
|
||||
configMan->volume = volume;
|
||||
configMan->save();
|
||||
}
|
||||
|
||||
void PlayerState::setShuffle(bool shuffle)
|
||||
{
|
||||
innerFrame.state.shuffle = shuffle;
|
||||
if (shuffle)
|
||||
{
|
||||
// Put current song at the begining
|
||||
std::swap(innerFrame.state.track[0], innerFrame.state.track[innerFrame.state.playing_track_index]);
|
||||
|
||||
// Shuffle current tracks
|
||||
for (int x = 1; x < innerFrame.state.track_count - 1; x++)
|
||||
{
|
||||
auto j = x + (std::rand() % (innerFrame.state.track_count - x));
|
||||
std::swap(innerFrame.state.track[j], innerFrame.state.track[x]);
|
||||
}
|
||||
innerFrame.state.playing_track_index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerState::setRepeat(bool repeat)
|
||||
{
|
||||
innerFrame.state.repeat = repeat;
|
||||
}
|
||||
|
||||
std::shared_ptr<TrackReference> PlayerState::getCurrentTrack()
|
||||
{
|
||||
// Wrap current track in a class
|
||||
return std::make_shared<TrackReference>(&innerFrame.state.track[innerFrame.state.playing_track_index]);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> PlayerState::encodeCurrentFrame(MessageType typ)
|
||||
{
|
||||
free(innerFrame.ident);
|
||||
free(innerFrame.protocol_version);
|
||||
|
||||
// Prepare current frame info
|
||||
innerFrame.version = 1;
|
||||
innerFrame.ident = strdup(deviceId);
|
||||
innerFrame.seq_nr = this->seqNum;
|
||||
innerFrame.protocol_version = strdup(protocolVersion);
|
||||
innerFrame.typ = typ;
|
||||
innerFrame.state_update_id = timeProvider->getSyncedTimestamp();
|
||||
innerFrame.has_version = true;
|
||||
innerFrame.has_seq_nr = true;
|
||||
innerFrame.recipient_count = 0;
|
||||
innerFrame.has_state = true;
|
||||
innerFrame.has_device_state = true;
|
||||
innerFrame.has_typ = true;
|
||||
innerFrame.has_state_update_id = true;
|
||||
|
||||
this->seqNum += 1;
|
||||
return pbEncode(Frame_fields, &innerFrame);
|
||||
}
|
||||
|
||||
// Wraps messy nanopb setters. @TODO: find a better way to handle this
|
||||
void PlayerState::addCapability(CapabilityType typ, int intValue, std::vector<std::string> stringValue)
|
||||
{
|
||||
innerFrame.device_state.capabilities[capabilityIndex].has_typ = true;
|
||||
this->innerFrame.device_state.capabilities[capabilityIndex].typ = typ;
|
||||
|
||||
if (intValue != -1)
|
||||
{
|
||||
this->innerFrame.device_state.capabilities[capabilityIndex].intValue[0] = intValue;
|
||||
this->innerFrame.device_state.capabilities[capabilityIndex].intValue_count = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
this->innerFrame.device_state.capabilities[capabilityIndex].intValue_count = 0;
|
||||
}
|
||||
|
||||
for (int x = 0; x < stringValue.size(); x++)
|
||||
{
|
||||
pbPutString(stringValue[x], this->innerFrame.device_state.capabilities[capabilityIndex].stringValue[x]);
|
||||
}
|
||||
|
||||
this->innerFrame.device_state.capabilities[capabilityIndex].stringValue_count = stringValue.size();
|
||||
this->capabilityIndex += 1;
|
||||
}
|
||||
@@ -1,189 +1,85 @@
|
||||
#include "Session.h"
|
||||
#include "MercuryManager.h"
|
||||
#include "Logger.h"
|
||||
#include <memory>
|
||||
#include "AuthChallenges.h"
|
||||
|
||||
using random_bytes_engine = std::independent_bits_engine<std::default_random_engine, CHAR_BIT, uint8_t>;
|
||||
using random_bytes_engine =
|
||||
std::independent_bits_engine<std::default_random_engine, CHAR_BIT, uint8_t>;
|
||||
|
||||
Session::Session()
|
||||
{
|
||||
this->clientHello = {};
|
||||
this->apResponse = {};
|
||||
this->authRequest = {};
|
||||
this->clientResPlaintext = {};
|
||||
using namespace cspot;
|
||||
|
||||
// Generates the public and priv key
|
||||
this->crypto = std::make_unique<Crypto>();
|
||||
this->shanConn = std::make_shared<ShannonConnection>();
|
||||
Session::Session() {
|
||||
this->challenges = std::make_unique<cspot::AuthChallenges>();
|
||||
}
|
||||
|
||||
Session::~Session()
|
||||
{
|
||||
pb_release(ClientHello_fields, &clientHello);
|
||||
pb_release(APResponseMessage_fields, &apResponse);
|
||||
pb_release(ClientResponsePlaintext_fields, &clientResPlaintext);
|
||||
Session::~Session() {}
|
||||
|
||||
void Session::connect(std::unique_ptr<cspot::PlainConnection> connection) {
|
||||
this->conn = std::move(connection);
|
||||
conn->timeoutHandler = [this]() {
|
||||
return this->triggerTimeout();
|
||||
};
|
||||
auto helloPacket = this->conn->sendPrefixPacket(
|
||||
{0x00, 0x04}, this->challenges->prepareClientHello());
|
||||
auto apResponse = this->conn->recvPacket();
|
||||
CSPOT_LOG(info, "Received APHello response");
|
||||
|
||||
auto solvedHello = this->challenges->solveApHello(helloPacket, apResponse);
|
||||
|
||||
conn->sendPrefixPacket({}, solvedHello);
|
||||
CSPOT_LOG(debug, "Received shannon keys");
|
||||
|
||||
// Generates the public and priv key
|
||||
this->shanConn = std::make_shared<ShannonConnection>();
|
||||
|
||||
// Init shanno-encrypted connection
|
||||
this->shanConn->wrapConnection(this->conn, challenges->shanSendKey,
|
||||
challenges->shanRecvKey);
|
||||
}
|
||||
|
||||
void Session::connect(std::unique_ptr<PlainConnection> connection)
|
||||
{
|
||||
this->conn = std::move(connection);
|
||||
auto helloPacket = this->sendClientHelloRequest();
|
||||
this->processAPHelloResponse(helloPacket);
|
||||
void Session::connectWithRandomAp() {
|
||||
auto apResolver = std::make_unique<ApResolve>("");
|
||||
auto conn = std::make_unique<cspot::PlainConnection>();
|
||||
conn->timeoutHandler = [this]() {
|
||||
return this->triggerTimeout();
|
||||
};
|
||||
|
||||
auto apAddr = apResolver->fetchFirstApAddress();
|
||||
|
||||
CSPOT_LOG(debug, "Connecting with AP <%s>", apAddr.c_str());
|
||||
conn->connect(apAddr);
|
||||
|
||||
this->connect(std::move(conn));
|
||||
}
|
||||
|
||||
void Session::connectWithRandomAp()
|
||||
{
|
||||
auto apResolver = std::make_unique<ApResolve>();
|
||||
this->conn = std::make_unique<PlainConnection>();
|
||||
std::vector<uint8_t> Session::authenticate(std::shared_ptr<LoginBlob> blob) {
|
||||
// save auth blob for reconnection purposes
|
||||
authBlob = blob;
|
||||
// prepare authentication request proto
|
||||
auto data = challenges->prepareAuthPacket(blob->authData, blob->authType,
|
||||
deviceId, blob->username);
|
||||
|
||||
auto apAddr = apResolver->fetchFirstApAddress();
|
||||
CSPOT_LOG(debug, "Connecting with AP <%s>", apAddr.c_str());
|
||||
this->conn->connectToAp(apAddr);
|
||||
auto helloPacket = this->sendClientHelloRequest();
|
||||
CSPOT_LOG(debug, "Sending APHello packet...");
|
||||
this->processAPHelloResponse(helloPacket);
|
||||
}
|
||||
// Send login request
|
||||
this->shanConn->sendPacket(LOGIN_REQUEST_COMMAND, data);
|
||||
|
||||
std::vector<uint8_t> Session::authenticate(std::shared_ptr<LoginBlob> blob)
|
||||
{
|
||||
// save auth blob for reconnection purposes
|
||||
authBlob = blob;
|
||||
|
||||
// prepare authentication request proto
|
||||
pbPutString(blob->username, authRequest.login_credentials.username);
|
||||
|
||||
std::copy(blob->authData.begin(), blob->authData.end(), authRequest.login_credentials.auth_data.bytes);
|
||||
authRequest.login_credentials.auth_data.size = blob->authData.size();
|
||||
|
||||
authRequest.login_credentials.typ = (AuthenticationType) blob->authType;
|
||||
authRequest.system_info.cpu_family = CpuFamily_CPU_UNKNOWN;
|
||||
authRequest.system_info.os = Os_OS_UNKNOWN;
|
||||
|
||||
auto infoStr = std::string(informationString);
|
||||
pbPutString(infoStr, authRequest.system_info.system_information_string);
|
||||
|
||||
auto deviceIdStr = std::string(deviceId);
|
||||
pbPutString(deviceId, authRequest.system_info.device_id);
|
||||
|
||||
auto versionStr = std::string(versionString);
|
||||
pbPutString(versionStr, authRequest.version_string);
|
||||
authRequest.has_version_string = true;
|
||||
|
||||
auto data = pbEncode(ClientResponseEncrypted_fields, &authRequest);
|
||||
|
||||
// Send login request
|
||||
this->shanConn->sendPacket(LOGIN_REQUEST_COMMAND, data);
|
||||
|
||||
auto packet = this->shanConn->recvPacket();
|
||||
switch (packet->command)
|
||||
{
|
||||
case AUTH_SUCCESSFUL_COMMAND:
|
||||
{
|
||||
CSPOT_LOG(debug, "Authorization successful");
|
||||
|
||||
// @TODO store the reusable credentials
|
||||
// PBWrapper<APWelcome> welcomePacket(packet->data)
|
||||
return std::vector<uint8_t>({0x1}); // TODO: return actual reusable credentaials to be stored somewhere
|
||||
break;
|
||||
auto packet = this->shanConn->recvPacket();
|
||||
switch (packet.command) {
|
||||
case AUTH_SUCCESSFUL_COMMAND: {
|
||||
CSPOT_LOG(debug, "Authorization successful");
|
||||
return std::vector<uint8_t>(
|
||||
{0x1}); // TODO: return actual reusable credentaials to be stored somewhere
|
||||
break;
|
||||
}
|
||||
case AUTH_DECLINED_COMMAND:
|
||||
{
|
||||
CSPOT_LOG(error, "Authorization declined");
|
||||
break;
|
||||
case AUTH_DECLINED_COMMAND: {
|
||||
CSPOT_LOG(error, "Authorization declined");
|
||||
break;
|
||||
}
|
||||
default:
|
||||
CSPOT_LOG(error, "Unknown auth fail code %d", packet->command);
|
||||
}
|
||||
CSPOT_LOG(error, "Unknown auth fail code %d", packet.command);
|
||||
}
|
||||
|
||||
return std::vector<uint8_t>(0);
|
||||
}
|
||||
|
||||
void Session::processAPHelloResponse(std::vector<uint8_t> &helloPacket)
|
||||
{
|
||||
CSPOT_LOG(debug, "Processing AP hello response...");
|
||||
auto data = this->conn->recvPacket();
|
||||
CSPOT_LOG(debug, "Received AP hello response");
|
||||
// Decode the response
|
||||
auto skipSize = std::vector<uint8_t>(data.begin() + 4, data.end());
|
||||
|
||||
pb_release(APResponseMessage_fields, &apResponse);
|
||||
pbDecode(apResponse, APResponseMessage_fields, skipSize);
|
||||
|
||||
auto diffieKey = std::vector<uint8_t>(apResponse.challenge.login_crypto_challenge.diffie_hellman.gs, apResponse.challenge.login_crypto_challenge.diffie_hellman.gs + 96);
|
||||
// Compute the diffie hellman shared key based on the response
|
||||
auto sharedKey = this->crypto->dhCalculateShared(diffieKey);
|
||||
|
||||
// Init client packet + Init server packets are required for the hmac challenge
|
||||
data.insert(data.begin(), helloPacket.begin(), helloPacket.end());
|
||||
|
||||
// Solve the hmac challenge
|
||||
auto resultData = std::vector<uint8_t>(0);
|
||||
for (int x = 1; x < 6; x++)
|
||||
{
|
||||
auto challengeVector = std::vector<uint8_t>(1);
|
||||
challengeVector[0] = x;
|
||||
|
||||
challengeVector.insert(challengeVector.begin(), data.begin(), data.end());
|
||||
auto digest = crypto->sha1HMAC(sharedKey, challengeVector);
|
||||
resultData.insert(resultData.end(), digest.begin(), digest.end());
|
||||
}
|
||||
|
||||
auto lastVec = std::vector<uint8_t>(resultData.begin(), resultData.begin() + 0x14);
|
||||
|
||||
// Digest generated!
|
||||
auto digest = crypto->sha1HMAC(lastVec, data);
|
||||
clientResPlaintext.login_crypto_response.has_diffie_hellman = true;
|
||||
|
||||
std::copy(digest.begin(),
|
||||
digest.end(),
|
||||
clientResPlaintext.login_crypto_response.diffie_hellman.hmac);
|
||||
|
||||
auto resultPacket = pbEncode(ClientResponsePlaintext_fields, &clientResPlaintext);
|
||||
|
||||
auto emptyPrefix = std::vector<uint8_t>(0);
|
||||
|
||||
this->conn->sendPrefixPacket(emptyPrefix, resultPacket);
|
||||
|
||||
// Get send and receive keys
|
||||
auto sendKey = std::vector<uint8_t>(resultData.begin() + 0x14, resultData.begin() + 0x34);
|
||||
auto recvKey = std::vector<uint8_t>(resultData.begin() + 0x34, resultData.begin() + 0x54);
|
||||
|
||||
CSPOT_LOG(debug, "Received shannon keys");
|
||||
|
||||
// Init shanno-encrypted connection
|
||||
this->shanConn->wrapConnection(this->conn, sendKey, recvKey);
|
||||
return std::vector<uint8_t>(0);
|
||||
}
|
||||
|
||||
void Session::close() {
|
||||
this->conn->closeSocket();
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Session::sendClientHelloRequest()
|
||||
{
|
||||
// Prepare protobuf message
|
||||
this->crypto->dhInit();
|
||||
|
||||
// Copy the public key into diffiehellman hello packet
|
||||
std::copy(this->crypto->publicKey.begin(),
|
||||
this->crypto->publicKey.end(),
|
||||
clientHello.login_crypto_hello.diffie_hellman.gc);
|
||||
|
||||
clientHello.login_crypto_hello.diffie_hellman.server_keys_known = 1;
|
||||
clientHello.build_info.product = Product_PRODUCT_CLIENT;
|
||||
clientHello.build_info.platform = Platform2_PLATFORM_LINUX_X86;
|
||||
clientHello.build_info.version = SPOTIFY_VERSION;
|
||||
clientHello.feature_set.autoupdate2 = true;
|
||||
clientHello.cryptosuites_supported[0] = Cryptosuite_CRYPTO_SUITE_SHANNON;
|
||||
clientHello.padding[0] = 0x1E;
|
||||
|
||||
clientHello.has_feature_set = true;
|
||||
clientHello.login_crypto_hello.has_diffie_hellman = true;
|
||||
clientHello.has_padding = true;
|
||||
clientHello.has_feature_set = true;
|
||||
|
||||
// Generate the random nonce
|
||||
auto nonce = crypto->generateVectorWithRandomData(16);
|
||||
std::copy(nonce.begin(), nonce.end(), clientHello.client_nonce);
|
||||
auto vecData = pbEncode(ClientHello_fields, &clientHello);
|
||||
auto prefix = std::vector<uint8_t>({0x00, 0x04});
|
||||
return this->conn->sendPrefixPacket(prefix, vecData);
|
||||
this->conn->close();
|
||||
}
|
||||
|
||||
@@ -9,16 +9,18 @@ using std::size_t;
|
||||
|
||||
static inline uint32_t rotl(uint32_t n, unsigned int c)
|
||||
{
|
||||
const unsigned int mask = (CHAR_BIT * sizeof(n) - 1); // assumes width is a power of 2.
|
||||
// assert ( (c<=mask) &&"rotate by type width or more");
|
||||
c &= sizeof(n) * CHAR_BIT - 1;
|
||||
return (n << c) | (n >> (sizeof(n)*CHAR_BIT-c));
|
||||
c &= mask;
|
||||
return (n << c) | (n >> ((-c) & mask));
|
||||
}
|
||||
|
||||
static inline uint32_t rotr(uint32_t n, unsigned int c)
|
||||
{
|
||||
const unsigned int mask = (CHAR_BIT * sizeof(n) - 1); // assumes width is a power of 2.
|
||||
// assert ( (c<=mask) &&"rotate by type width or more");
|
||||
c &= sizeof(n) * CHAR_BIT - 1;
|
||||
return (n >> c) | (n << (sizeof(n)*CHAR_BIT-c));
|
||||
c &= mask;
|
||||
return (n >> c) | (n << ((-c) & mask));
|
||||
}
|
||||
|
||||
uint32_t Shannon::sbox1(uint32_t w)
|
||||
@@ -140,7 +142,7 @@ void Shannon::diffuse()
|
||||
#define ADDKEY(k) \
|
||||
this->R[KEYP] ^= (k);
|
||||
|
||||
void Shannon::loadKey(const std::vector<uint8_t> &key)
|
||||
void Shannon::loadKey(const std::vector<uint8_t>& key)
|
||||
{
|
||||
int i, j;
|
||||
uint32_t k;
|
||||
@@ -182,7 +184,7 @@ void Shannon::loadKey(const std::vector<uint8_t> &key)
|
||||
this->R[i] ^= this->CRC[i];
|
||||
}
|
||||
|
||||
void Shannon::key(const std::vector<uint8_t> &key)
|
||||
void Shannon::key(const std::vector<uint8_t>& key)
|
||||
{
|
||||
this->initState();
|
||||
this->loadKey(key);
|
||||
@@ -191,7 +193,7 @@ void Shannon::key(const std::vector<uint8_t> &key)
|
||||
this->nbuf = 0;
|
||||
}
|
||||
|
||||
void Shannon::nonce(const std::vector<uint8_t> &nonce)
|
||||
void Shannon::nonce(const std::vector<uint8_t>& nonce)
|
||||
{
|
||||
this->reloadState();
|
||||
this->konst = Shannon::INITKONST;
|
||||
@@ -200,11 +202,11 @@ void Shannon::nonce(const std::vector<uint8_t> &nonce)
|
||||
this->nbuf = 0;
|
||||
}
|
||||
|
||||
void Shannon::stream(std::vector<uint8_t> &bufVec)
|
||||
void Shannon::stream(std::vector<uint8_t>& bufVec)
|
||||
{
|
||||
uint8_t *endbuf;
|
||||
uint8_t* endbuf;
|
||||
size_t nbytes = bufVec.size();
|
||||
uint8_t *buf = bufVec.data();
|
||||
uint8_t* buf = bufVec.data();
|
||||
/* handle any previously buffered bytes */
|
||||
while (this->nbuf != 0 && nbytes != 0)
|
||||
{
|
||||
@@ -239,12 +241,12 @@ void Shannon::stream(std::vector<uint8_t> &bufVec)
|
||||
}
|
||||
}
|
||||
|
||||
void Shannon::maconly(std::vector<uint8_t> &bufVec)
|
||||
void Shannon::maconly(std::vector<uint8_t>& bufVec)
|
||||
{
|
||||
size_t nbytes = bufVec.size();
|
||||
uint8_t *buf = bufVec.data();
|
||||
uint8_t* buf = bufVec.data();
|
||||
|
||||
uint8_t *endbuf;
|
||||
uint8_t* endbuf;
|
||||
|
||||
/* handle any previously buffered bytes */
|
||||
if (this->nbuf != 0)
|
||||
@@ -286,11 +288,11 @@ void Shannon::maconly(std::vector<uint8_t> &bufVec)
|
||||
}
|
||||
}
|
||||
|
||||
void Shannon::encrypt(std::vector<uint8_t> &bufVec)
|
||||
void Shannon::encrypt(std::vector<uint8_t>& bufVec)
|
||||
{
|
||||
size_t nbytes = bufVec.size();
|
||||
uint8_t *buf = bufVec.data();
|
||||
uint8_t *endbuf;
|
||||
uint8_t* buf = bufVec.data();
|
||||
uint8_t* endbuf;
|
||||
uint32_t t = 0;
|
||||
|
||||
/* handle any previously buffered bytes */
|
||||
@@ -341,11 +343,11 @@ void Shannon::encrypt(std::vector<uint8_t> &bufVec)
|
||||
}
|
||||
|
||||
|
||||
void Shannon::decrypt(std::vector<uint8_t> &bufVec)
|
||||
void Shannon::decrypt(std::vector<uint8_t>& bufVec)
|
||||
{
|
||||
size_t nbytes = bufVec.size();
|
||||
uint8_t *buf = bufVec.data();
|
||||
uint8_t *endbuf;
|
||||
uint8_t* buf = bufVec.data();
|
||||
uint8_t* endbuf;
|
||||
uint32_t t = 0;
|
||||
|
||||
/* handle any previously buffered bytes */
|
||||
@@ -394,10 +396,10 @@ void Shannon::decrypt(std::vector<uint8_t> &bufVec)
|
||||
}
|
||||
}
|
||||
|
||||
void Shannon::finish(std::vector<uint8_t> &bufVec)
|
||||
void Shannon::finish(std::vector<uint8_t>& bufVec)
|
||||
{
|
||||
size_t nbytes = bufVec.size();
|
||||
uint8_t *buf = bufVec.data();
|
||||
uint8_t* buf = bufVec.data();
|
||||
int i;
|
||||
|
||||
/* handle any previously buffered bytes */
|
||||
@@ -408,10 +410,10 @@ void Shannon::finish(std::vector<uint8_t> &bufVec)
|
||||
}
|
||||
|
||||
/* perturb the MAC to mark end of input.
|
||||
* Note that only the stream register is updated, not the CRC. This is an
|
||||
* action that can't be duplicated by passing in plaintext, hence
|
||||
* defeating any kind of extension attack.
|
||||
*/
|
||||
* Note that only the stream register is updated, not the CRC. This is an
|
||||
* action that can't be duplicated by passing in plaintext, hence
|
||||
* defeating any kind of extension attack.
|
||||
*/
|
||||
this->cycle();
|
||||
ADDKEY(INITKONST ^ (this->nbuf << 3));
|
||||
this->nbuf = 0;
|
||||
|
||||
@@ -1,100 +1,96 @@
|
||||
#include "ShannonConnection.h"
|
||||
#include "Logger.h"
|
||||
#include "Packet.h"
|
||||
|
||||
ShannonConnection::ShannonConnection()
|
||||
{
|
||||
using namespace cspot;
|
||||
|
||||
ShannonConnection::ShannonConnection() {}
|
||||
|
||||
ShannonConnection::~ShannonConnection() {}
|
||||
|
||||
void ShannonConnection::wrapConnection(
|
||||
std::shared_ptr<cspot::PlainConnection> conn, std::vector<uint8_t>& sendKey,
|
||||
std::vector<uint8_t>& recvKey) {
|
||||
this->conn = conn;
|
||||
|
||||
this->sendCipher = std::make_unique<Shannon>();
|
||||
this->recvCipher = std::make_unique<Shannon>();
|
||||
|
||||
// Set keys
|
||||
this->sendCipher->key(sendKey);
|
||||
this->recvCipher->key(recvKey);
|
||||
|
||||
// Set initial nonce
|
||||
this->sendCipher->nonce(pack<uint32_t>(htonl(0)));
|
||||
this->recvCipher->nonce(pack<uint32_t>(htonl(0)));
|
||||
}
|
||||
|
||||
ShannonConnection::~ShannonConnection()
|
||||
{
|
||||
void ShannonConnection::sendPacket(uint8_t cmd, std::vector<uint8_t>& data) {
|
||||
std::scoped_lock lock(this->writeMutex);
|
||||
auto rawPacket = this->cipherPacket(cmd, data);
|
||||
|
||||
// Shannon encrypt the packet and write it to sock
|
||||
this->sendCipher->encrypt(rawPacket);
|
||||
this->conn->writeBlock(rawPacket);
|
||||
|
||||
// Generate mac
|
||||
std::vector<uint8_t> mac(MAC_SIZE);
|
||||
this->sendCipher->finish(mac);
|
||||
|
||||
// Update the nonce
|
||||
this->sendNonce += 1;
|
||||
this->sendCipher->nonce(pack<uint32_t>(htonl(this->sendNonce)));
|
||||
|
||||
// Write the mac to sock
|
||||
this->conn->writeBlock(mac);
|
||||
}
|
||||
|
||||
void ShannonConnection::wrapConnection(std::shared_ptr<PlainConnection> conn, std::vector<uint8_t> &sendKey, std::vector<uint8_t> &recvKey)
|
||||
{
|
||||
this->conn = conn;
|
||||
cspot::Packet ShannonConnection::recvPacket() {
|
||||
std::scoped_lock lock(this->readMutex);
|
||||
|
||||
this->sendCipher = std::make_unique<Shannon>();
|
||||
this->recvCipher = std::make_unique<Shannon>();
|
||||
std::vector<uint8_t> data(3);
|
||||
// Receive 3 bytes, cmd + int16 size
|
||||
this->conn->readBlock(data.data(), 3);
|
||||
this->recvCipher->decrypt(data);
|
||||
|
||||
// Set keys
|
||||
this->sendCipher->key(sendKey);
|
||||
this->recvCipher->key(recvKey);
|
||||
auto readSize = ntohs(extract<uint16_t>(data, 1));
|
||||
auto packetData = std::vector<uint8_t>(readSize);
|
||||
|
||||
// Set initial nonce
|
||||
this->sendCipher->nonce(pack<uint32_t>(htonl(0)));
|
||||
this->recvCipher->nonce(pack<uint32_t>(htonl(0)));
|
||||
// Read and decode if the packet has an actual body
|
||||
if (readSize > 0) {
|
||||
this->conn->readBlock(packetData.data(), readSize);
|
||||
this->recvCipher->decrypt(packetData);
|
||||
}
|
||||
|
||||
// Read mac
|
||||
std::vector<uint8_t> mac(MAC_SIZE);
|
||||
this->conn->readBlock(mac.data(), MAC_SIZE);
|
||||
|
||||
// Generate mac
|
||||
std::vector<uint8_t> mac2(MAC_SIZE);
|
||||
this->recvCipher->finish(mac2);
|
||||
|
||||
if (mac != mac2) {
|
||||
CSPOT_LOG(error, "Shannon read: Mac doesn't match");
|
||||
}
|
||||
|
||||
// Update the nonce
|
||||
this->recvNonce += 1;
|
||||
this->recvCipher->nonce(pack<uint32_t>(htonl(this->recvNonce)));
|
||||
uint8_t cmd = 0;
|
||||
if (data.size() > 0) {
|
||||
cmd = data[0];
|
||||
}
|
||||
// data[0] == cmd
|
||||
return Packet{cmd, packetData};
|
||||
}
|
||||
|
||||
void ShannonConnection::sendPacket(uint8_t cmd, std::vector<uint8_t> &data)
|
||||
{
|
||||
this->writeMutex.lock();
|
||||
auto rawPacket = this->cipherPacket(cmd, data);
|
||||
std::vector<uint8_t> ShannonConnection::cipherPacket(
|
||||
uint8_t cmd, std::vector<uint8_t>& data) {
|
||||
// Generate packet structure, [Command] [Size] [Raw data]
|
||||
auto sizeRaw = pack<uint16_t>(htons(uint16_t(data.size())));
|
||||
|
||||
// Shannon encrypt the packet and write it to sock
|
||||
this->sendCipher->encrypt(rawPacket);
|
||||
this->conn->writeBlock(rawPacket);
|
||||
sizeRaw.insert(sizeRaw.begin(), cmd);
|
||||
sizeRaw.insert(sizeRaw.end(), data.begin(), data.end());
|
||||
|
||||
// Generate mac
|
||||
std::vector<uint8_t> mac(MAC_SIZE);
|
||||
this->sendCipher->finish(mac);
|
||||
|
||||
// Update the nonce
|
||||
this->sendNonce += 1;
|
||||
this->sendCipher->nonce(pack<uint32_t>(htonl(this->sendNonce)));
|
||||
|
||||
// Write the mac to sock
|
||||
this->conn->writeBlock(mac);
|
||||
this->writeMutex.unlock();
|
||||
}
|
||||
|
||||
std::unique_ptr<Packet> ShannonConnection::recvPacket()
|
||||
{
|
||||
this->readMutex.lock();
|
||||
// Receive 3 bytes, cmd + int16 size
|
||||
auto data = this->conn->readBlock(3);
|
||||
this->recvCipher->decrypt(data);
|
||||
|
||||
auto packetData = std::vector<uint8_t>();
|
||||
|
||||
auto readSize = ntohs(extract<uint16_t>(data, 1));
|
||||
|
||||
// Read and decode if the packet has an actual body
|
||||
if (readSize > 0)
|
||||
{
|
||||
packetData = this->conn->readBlock(readSize);
|
||||
this->recvCipher->decrypt(packetData);
|
||||
}
|
||||
|
||||
// Read mac
|
||||
auto mac = this->conn->readBlock(MAC_SIZE);
|
||||
|
||||
// Generate mac
|
||||
std::vector<uint8_t> mac2(MAC_SIZE);
|
||||
this->recvCipher->finish(mac2);
|
||||
|
||||
if (mac != mac2)
|
||||
{
|
||||
CSPOT_LOG(error, "Shannon read: Mac doesn't match");
|
||||
}
|
||||
|
||||
// Update the nonce
|
||||
this->recvNonce += 1;
|
||||
this->recvCipher->nonce(pack<uint32_t>(htonl(this->recvNonce)));
|
||||
|
||||
// Unlock the mutex
|
||||
this->readMutex.unlock();
|
||||
|
||||
// data[0] == cmd
|
||||
return std::make_unique<Packet>(data[0], packetData);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> ShannonConnection::cipherPacket(uint8_t cmd, std::vector<uint8_t> &data)
|
||||
{
|
||||
// Generate packet structure, [Command] [Size] [Raw data]
|
||||
auto sizeRaw = pack<uint16_t>(htons(uint16_t(data.size())));
|
||||
|
||||
sizeRaw.insert(sizeRaw.begin(), cmd);
|
||||
sizeRaw.insert(sizeRaw.end(), data.begin(), data.end());
|
||||
|
||||
return sizeRaw;
|
||||
return sizeRaw;
|
||||
}
|
||||
|
||||
@@ -1,244 +0,0 @@
|
||||
#include "SpircController.h"
|
||||
#include "ConfigJSON.h"
|
||||
#include "Logger.h"
|
||||
#include "SpotifyTrack.h"
|
||||
|
||||
SpircController::SpircController(std::shared_ptr<MercuryManager> manager,
|
||||
std::string username,
|
||||
std::shared_ptr<AudioSink> audioSink) {
|
||||
|
||||
this->manager = manager;
|
||||
this->player = std::make_unique<Player>(manager, audioSink);
|
||||
this->state = std::make_unique<PlayerState>(manager->timeProvider);
|
||||
this->username = username;
|
||||
|
||||
player->endOfFileCallback = [=]() {
|
||||
if (state->nextTrack()) {
|
||||
loadTrack();
|
||||
}
|
||||
};
|
||||
|
||||
player->setVolume(configMan->volume);
|
||||
subscribe();
|
||||
}
|
||||
|
||||
SpircController::~SpircController() {
|
||||
}
|
||||
|
||||
void SpircController::subscribe() {
|
||||
mercuryCallback responseLambda = [=](std::unique_ptr<MercuryResponse> res) {
|
||||
// this->trackInformationCallback(std::move(res));
|
||||
sendCmd(MessageType_kMessageTypeHello);
|
||||
CSPOT_LOG(debug, "Sent kMessageTypeHello!");
|
||||
};
|
||||
mercuryCallback subLambda = [=](std::unique_ptr<MercuryResponse> res) {
|
||||
this->handleFrame(res->parts[0]);
|
||||
};
|
||||
|
||||
manager->execute(MercuryType::SUB,
|
||||
"hm://remote/user/" + this->username + "/", responseLambda,
|
||||
subLambda);
|
||||
}
|
||||
|
||||
void SpircController::setPause(bool isPaused, bool notifyPlayer) {
|
||||
sendEvent(CSpotEventType::PLAY_PAUSE, isPaused);
|
||||
if (isPaused) {
|
||||
CSPOT_LOG(debug, "External pause command");
|
||||
if (notifyPlayer) player->pause();
|
||||
state->setPlaybackState(PlaybackState::Paused);
|
||||
} else {
|
||||
CSPOT_LOG(debug, "External play command");
|
||||
if (notifyPlayer) player->play();
|
||||
state->setPlaybackState(PlaybackState::Playing);
|
||||
}
|
||||
notify();
|
||||
}
|
||||
|
||||
void SpircController::disconnect(void) {
|
||||
player->cancelCurrentTrack();
|
||||
state->setActive(false);
|
||||
notify();
|
||||
// Send the event at the end at it might be a last gasp
|
||||
sendEvent(CSpotEventType::DISC);
|
||||
}
|
||||
|
||||
void SpircController::playToggle() {
|
||||
if (state->innerFrame.state.status == PlayStatus_kPlayStatusPause) {
|
||||
setPause(false);
|
||||
} else {
|
||||
setPause(true);
|
||||
}
|
||||
}
|
||||
|
||||
void SpircController::adjustVolume(int by) {
|
||||
if (state->innerFrame.device_state.has_volume) {
|
||||
int volume = state->innerFrame.device_state.volume + by;
|
||||
if (volume < 0) volume = 0;
|
||||
else if (volume > MAX_VOLUME) volume = MAX_VOLUME;
|
||||
setVolume(volume);
|
||||
}
|
||||
}
|
||||
|
||||
void SpircController::setVolume(int volume) {
|
||||
setRemoteVolume(volume);
|
||||
player->setVolume(volume);
|
||||
configMan->save();
|
||||
}
|
||||
|
||||
void SpircController::setRemoteVolume(int volume) {
|
||||
state->setVolume(volume);
|
||||
notify();
|
||||
}
|
||||
|
||||
void SpircController::nextSong() {
|
||||
if (state->nextTrack()) {
|
||||
loadTrack();
|
||||
} else {
|
||||
player->cancelCurrentTrack();
|
||||
}
|
||||
notify();
|
||||
}
|
||||
|
||||
void SpircController::prevSong() {
|
||||
state->prevTrack();
|
||||
loadTrack();
|
||||
notify();
|
||||
}
|
||||
|
||||
void SpircController::handleFrame(std::vector<uint8_t> &data) {
|
||||
pb_release(Frame_fields, &state->remoteFrame);
|
||||
pbDecode(state->remoteFrame, Frame_fields, data);
|
||||
//CSPOT_LOG(info, "FRAME RECEIVED %d", (int) state->remoteFrame.typ);
|
||||
switch (state->remoteFrame.typ) {
|
||||
case MessageType_kMessageTypeNotify: {
|
||||
CSPOT_LOG(debug, "Notify frame");
|
||||
// Pause the playback if another player took control
|
||||
if (state->isActive() &&
|
||||
state->remoteFrame.device_state.is_active) {
|
||||
disconnect();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MessageType_kMessageTypeSeek: {
|
||||
CSPOT_LOG(debug, "Seek command");
|
||||
sendEvent(CSpotEventType::SEEK, (int) state->remoteFrame.position);
|
||||
state->updatePositionMs(state->remoteFrame.position);
|
||||
this->player->seekMs(state->remoteFrame.position);
|
||||
notify();
|
||||
break;
|
||||
}
|
||||
case MessageType_kMessageTypeVolume:
|
||||
sendEvent(CSpotEventType::VOLUME, (int) state->remoteFrame.volume);
|
||||
setVolume(state->remoteFrame.volume);
|
||||
break;
|
||||
case MessageType_kMessageTypePause:
|
||||
setPause(true);
|
||||
break;
|
||||
case MessageType_kMessageTypePlay:
|
||||
setPause(false);
|
||||
break;
|
||||
case MessageType_kMessageTypeNext:
|
||||
sendEvent(CSpotEventType::NEXT);
|
||||
nextSong();
|
||||
break;
|
||||
case MessageType_kMessageTypePrev:
|
||||
sendEvent(CSpotEventType::PREV);
|
||||
prevSong();
|
||||
break;
|
||||
case MessageType_kMessageTypeLoad: {
|
||||
CSPOT_LOG(debug, "Load frame!");
|
||||
|
||||
state->setActive(true);
|
||||
|
||||
// Every sane person on the planet would expect std::move to work here.
|
||||
// And it does... on every single platform EXCEPT for ESP32 for some
|
||||
// reason. For which it corrupts memory and makes printf fail. so yeah.
|
||||
// its cursed.
|
||||
state->updateTracks();
|
||||
|
||||
// bool isPaused = (state->remoteFrame.state->status.value() ==
|
||||
// PlayStatus::kPlayStatusPlay) ? false : true;
|
||||
loadTrack(state->remoteFrame.state.position_ms, false);
|
||||
state->updatePositionMs(state->remoteFrame.state.position_ms);
|
||||
|
||||
this->notify();
|
||||
break;
|
||||
}
|
||||
case MessageType_kMessageTypeReplace: {
|
||||
CSPOT_LOG(debug, "Got replace frame!");
|
||||
state->updateTracks();
|
||||
this->notify();
|
||||
break;
|
||||
}
|
||||
case MessageType_kMessageTypeShuffle: {
|
||||
CSPOT_LOG(debug, "Got shuffle frame");
|
||||
state->setShuffle(state->remoteFrame.state.shuffle);
|
||||
this->notify();
|
||||
break;
|
||||
}
|
||||
case MessageType_kMessageTypeRepeat: {
|
||||
CSPOT_LOG(debug, "Got repeat frame");
|
||||
state->setRepeat(state->remoteFrame.state.repeat);
|
||||
this->notify();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void SpircController::loadTrack(uint32_t position_ms, bool isPaused) {
|
||||
sendEvent(CSpotEventType::LOAD, (int) position_ms);
|
||||
state->setPlaybackState(PlaybackState::Loading);
|
||||
std::function<void(bool)> loadedLambda = [=](bool needFlush) {
|
||||
// Loading finished, notify that playback started
|
||||
setPause(isPaused, false);
|
||||
sendEvent(CSpotEventType::PLAYBACK_START, needFlush);
|
||||
};
|
||||
|
||||
player->handleLoad(state->getCurrentTrack(), loadedLambda, position_ms,
|
||||
isPaused);
|
||||
}
|
||||
|
||||
void SpircController::notify() {
|
||||
this->sendCmd(MessageType_kMessageTypeNotify);
|
||||
}
|
||||
|
||||
void SpircController::sendEvent(CSpotEventType eventType, std::variant<TrackInfo, int, bool> data) {
|
||||
if (eventHandler != nullptr) {
|
||||
CSpotEvent event = {
|
||||
.eventType = eventType,
|
||||
.data = data,
|
||||
};
|
||||
|
||||
eventHandler(event);
|
||||
}
|
||||
}
|
||||
|
||||
void SpircController::setEventHandler(cspotEventHandler callback) {
|
||||
this->eventHandler = callback;
|
||||
|
||||
player->trackChanged = ([this](TrackInfo &track) {
|
||||
TrackInfo info;
|
||||
info.album = track.album;
|
||||
info.artist = track.artist;
|
||||
info.imageUrl = track.imageUrl;
|
||||
info.name = track.name;
|
||||
info.duration = track.duration;
|
||||
this->sendEvent(CSpotEventType::TRACK_INFO, info);
|
||||
});
|
||||
}
|
||||
|
||||
void SpircController::stopPlayer() { this->player->stop(); }
|
||||
|
||||
void SpircController::sendCmd(MessageType typ) {
|
||||
// Serialize current player state
|
||||
auto encodedFrame = state->encodeCurrentFrame(typ);
|
||||
|
||||
mercuryCallback responseLambda = [=](std::unique_ptr<MercuryResponse> res) {
|
||||
};
|
||||
auto parts = mercuryParts({encodedFrame});
|
||||
this->manager->execute(MercuryType::SEND,
|
||||
"hm://remote/user/" + this->username + "/",
|
||||
responseLambda, parts);
|
||||
}
|
||||
319
components/spotify/cspot/src/SpircHandler.cpp
Normal file
319
components/spotify/cspot/src/SpircHandler.cpp
Normal file
@@ -0,0 +1,319 @@
|
||||
#include "SpircHandler.h"
|
||||
#include <memory>
|
||||
#include "AccessKeyFetcher.h"
|
||||
#include "BellUtils.h"
|
||||
#include "CSpotContext.h"
|
||||
#include "Logger.h"
|
||||
#include "MercurySession.h"
|
||||
#include "PlaybackState.h"
|
||||
#include "TrackPlayer.h"
|
||||
#include "TrackReference.h"
|
||||
#include "protobuf/spirc.pb.h"
|
||||
|
||||
using namespace cspot;
|
||||
|
||||
SpircHandler::SpircHandler(std::shared_ptr<cspot::Context> ctx)
|
||||
: playbackState(ctx) {
|
||||
|
||||
auto isAiringCallback = [this]() {
|
||||
return !(isNextTrackPreloaded || isRequestedFromLoad);
|
||||
};
|
||||
|
||||
auto EOFCallback = [this]() {
|
||||
auto ref = this->playbackState.getNextTrackRef();
|
||||
|
||||
if (!isNextTrackPreloaded && !isRequestedFromLoad && ref != nullptr) {
|
||||
isNextTrackPreloaded = true;
|
||||
auto trackRef = TrackReference::fromTrackRef(ref);
|
||||
this->trackPlayer->loadTrackFromRef(trackRef, 0, true);
|
||||
}
|
||||
|
||||
if (ref == nullptr) {
|
||||
sendEvent(EventType::DEPLETED);
|
||||
}
|
||||
};
|
||||
|
||||
auto trackLoadedCallback = [this]() {
|
||||
this->currentTrackInfo = this->trackPlayer->getCurrentTrackInfo();
|
||||
|
||||
if (isRequestedFromLoad) {
|
||||
sendEvent(EventType::PLAYBACK_START, (int)nextTrackPosition);
|
||||
setPause(false);
|
||||
}
|
||||
};
|
||||
|
||||
this->ctx = ctx;
|
||||
this->trackPlayer = std::make_shared<TrackPlayer>(ctx, isAiringCallback, EOFCallback, trackLoadedCallback);
|
||||
|
||||
// Subscribe to mercury on session ready
|
||||
ctx->session->setConnectedHandler([this]() { this->subscribeToMercury(); });
|
||||
}
|
||||
|
||||
void SpircHandler::subscribeToMercury() {
|
||||
auto responseLambda = [this](MercurySession::Response& res) {
|
||||
if (res.fail)
|
||||
return;
|
||||
|
||||
sendCmd(MessageType_kMessageTypeHello);
|
||||
CSPOT_LOG(debug, "Sent kMessageTypeHello!");
|
||||
|
||||
// Assign country code
|
||||
this->ctx->config.countryCode = this->ctx->session->getCountryCode();
|
||||
};
|
||||
auto subscriptionLambda = [this](MercurySession::Response& res) {
|
||||
if (res.fail)
|
||||
return;
|
||||
CSPOT_LOG(debug, "Received subscription response");
|
||||
|
||||
this->handleFrame(res.parts[0]);
|
||||
};
|
||||
|
||||
ctx->session->executeSubscription(
|
||||
MercurySession::RequestType::SUB,
|
||||
"hm://remote/user/" + ctx->config.username + "/", responseLambda,
|
||||
subscriptionLambda);
|
||||
}
|
||||
|
||||
void SpircHandler::loadTrackFromURI(const std::string& uri) {
|
||||
// {track/episode}:{gid}
|
||||
bool isEpisode = uri.find("episode:") != std::string::npos;
|
||||
auto gid = stringHexToBytes(uri.substr(uri.find(":") + 1));
|
||||
auto trackRef = TrackReference::fromGID(gid, isEpisode);
|
||||
|
||||
isRequestedFromLoad = true;
|
||||
isNextTrackPreloaded = false;
|
||||
|
||||
playbackState.setActive(true);
|
||||
|
||||
auto playbackRef = playbackState.getCurrentTrackRef();
|
||||
|
||||
if (playbackRef != nullptr) {
|
||||
playbackState.updatePositionMs(playbackState.remoteFrame.state.position_ms);
|
||||
|
||||
auto ref = TrackReference::fromTrackRef(playbackRef);
|
||||
this->trackPlayer->loadTrackFromRef(
|
||||
ref, playbackState.remoteFrame.state.position_ms, true);
|
||||
playbackState.setPlaybackState(PlaybackState::State::Loading);
|
||||
this->nextTrackPosition = playbackState.remoteFrame.state.position_ms;
|
||||
}
|
||||
|
||||
this->notify();
|
||||
}
|
||||
|
||||
void SpircHandler::notifyAudioReachedPlayback() {
|
||||
if (isRequestedFromLoad || isNextTrackPreloaded) {
|
||||
playbackState.updatePositionMs(nextTrackPosition);
|
||||
playbackState.setPlaybackState(PlaybackState::State::Playing);
|
||||
} else {
|
||||
setPause(true);
|
||||
}
|
||||
|
||||
isRequestedFromLoad = false;
|
||||
|
||||
if (isNextTrackPreloaded) {
|
||||
isNextTrackPreloaded = false;
|
||||
|
||||
playbackState.nextTrack();
|
||||
nextTrackPosition = 0;
|
||||
}
|
||||
|
||||
this->notify();
|
||||
|
||||
sendEvent(EventType::TRACK_INFO, this->trackPlayer->getCurrentTrackInfo());
|
||||
}
|
||||
|
||||
void SpircHandler::updatePositionMs(uint32_t position) {
|
||||
playbackState.updatePositionMs(position);
|
||||
notify();
|
||||
}
|
||||
|
||||
void SpircHandler::disconnect() {
|
||||
this->trackPlayer->stopTrack();
|
||||
this->ctx->session->disconnect();
|
||||
}
|
||||
|
||||
void SpircHandler::handleFrame(std::vector<uint8_t>& data) {
|
||||
pb_release(Frame_fields, &playbackState.remoteFrame);
|
||||
pbDecode(playbackState.remoteFrame, Frame_fields, data);
|
||||
|
||||
switch (playbackState.remoteFrame.typ) {
|
||||
case MessageType_kMessageTypeNotify: {
|
||||
CSPOT_LOG(debug, "Notify frame");
|
||||
|
||||
// Pause the playback if another player took control
|
||||
if (playbackState.isActive() &&
|
||||
playbackState.remoteFrame.device_state.is_active) {
|
||||
CSPOT_LOG(debug, "Another player took control, pausing playback");
|
||||
playbackState.setActive(false);
|
||||
this->trackPlayer->stopTrack();
|
||||
sendEvent(EventType::DISC);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MessageType_kMessageTypeSeek: {
|
||||
/* If next track is already downloading, we can't seek in the current one anymore. Also,
|
||||
* when last track has been reached, we has to restart as we can't tell the difference */
|
||||
if ((!isNextTrackPreloaded && this->playbackState.getNextTrackRef()) || isRequestedFromLoad) {
|
||||
CSPOT_LOG(debug, "Seek command while streaming current");
|
||||
sendEvent(EventType::SEEK, (int)playbackState.remoteFrame.position);
|
||||
playbackState.updatePositionMs(playbackState.remoteFrame.position);
|
||||
trackPlayer->seekMs(playbackState.remoteFrame.position);
|
||||
} else {
|
||||
CSPOT_LOG(debug, "Seek command while streaming next or before started");
|
||||
isRequestedFromLoad = true;
|
||||
isNextTrackPreloaded = false;
|
||||
auto ref = TrackReference::fromTrackRef(playbackState.getCurrentTrackRef());
|
||||
this->trackPlayer->loadTrackFromRef(ref, playbackState.remoteFrame.position, true);
|
||||
this->nextTrackPosition = playbackState.remoteFrame.position;
|
||||
}
|
||||
notify();
|
||||
break;
|
||||
}
|
||||
case MessageType_kMessageTypeVolume:
|
||||
playbackState.setVolume(playbackState.remoteFrame.volume);
|
||||
this->notify();
|
||||
sendEvent(EventType::VOLUME, (int)playbackState.remoteFrame.volume);
|
||||
break;
|
||||
case MessageType_kMessageTypePause:
|
||||
setPause(true);
|
||||
break;
|
||||
case MessageType_kMessageTypePlay:
|
||||
setPause(false);
|
||||
break;
|
||||
case MessageType_kMessageTypeNext:
|
||||
nextSong();
|
||||
sendEvent(EventType::NEXT);
|
||||
break;
|
||||
case MessageType_kMessageTypePrev:
|
||||
previousSong();
|
||||
sendEvent(EventType::PREV);
|
||||
break;
|
||||
case MessageType_kMessageTypeLoad: {
|
||||
CSPOT_LOG(debug, "Load frame!");
|
||||
isRequestedFromLoad = true;
|
||||
isNextTrackPreloaded = false;
|
||||
|
||||
playbackState.setActive(true);
|
||||
playbackState.updateTracks();
|
||||
|
||||
auto playbackRef = playbackState.getCurrentTrackRef();
|
||||
|
||||
if (playbackRef != nullptr) {
|
||||
playbackState.updatePositionMs(
|
||||
playbackState.remoteFrame.state.position_ms);
|
||||
|
||||
auto ref = TrackReference::fromTrackRef(playbackRef);
|
||||
this->trackPlayer->loadTrackFromRef(
|
||||
ref, playbackState.remoteFrame.state.position_ms, true);
|
||||
playbackState.setPlaybackState(PlaybackState::State::Loading);
|
||||
this->nextTrackPosition = playbackState.remoteFrame.state.position_ms;
|
||||
}
|
||||
|
||||
this->notify();
|
||||
break;
|
||||
}
|
||||
case MessageType_kMessageTypeReplace: {
|
||||
CSPOT_LOG(debug, "Got replace frame");
|
||||
playbackState.updateTracks();
|
||||
this->notify();
|
||||
break;
|
||||
}
|
||||
case MessageType_kMessageTypeShuffle: {
|
||||
CSPOT_LOG(debug, "Got shuffle frame");
|
||||
playbackState.setShuffle(playbackState.remoteFrame.state.shuffle);
|
||||
this->notify();
|
||||
break;
|
||||
}
|
||||
case MessageType_kMessageTypeRepeat: {
|
||||
CSPOT_LOG(debug, "Got repeat frame");
|
||||
playbackState.setRepeat(playbackState.remoteFrame.state.repeat);
|
||||
this->notify();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void SpircHandler::setRemoteVolume(int volume) {
|
||||
playbackState.setVolume(volume);
|
||||
notify();
|
||||
}
|
||||
|
||||
void SpircHandler::notify() {
|
||||
this->sendCmd(MessageType_kMessageTypeNotify);
|
||||
}
|
||||
|
||||
void SpircHandler::nextSong() {
|
||||
if (playbackState.nextTrack()) {
|
||||
isRequestedFromLoad = true;
|
||||
isNextTrackPreloaded = false;
|
||||
auto ref = TrackReference::fromTrackRef(playbackState.getCurrentTrackRef());
|
||||
this->trackPlayer->loadTrackFromRef(ref, 0, true);
|
||||
} else {
|
||||
sendEvent(EventType::FLUSH);
|
||||
playbackState.updatePositionMs(0);
|
||||
trackPlayer->stopTrack();
|
||||
}
|
||||
this->nextTrackPosition = 0;
|
||||
notify();
|
||||
}
|
||||
|
||||
void SpircHandler::previousSong() {
|
||||
playbackState.prevTrack();
|
||||
isRequestedFromLoad = true;
|
||||
isNextTrackPreloaded = false;
|
||||
|
||||
sendEvent(EventType::PREV);
|
||||
auto ref = TrackReference::fromTrackRef(playbackState.getCurrentTrackRef());
|
||||
this->trackPlayer->loadTrackFromRef(ref, 0, true);
|
||||
this->nextTrackPosition = 0;
|
||||
|
||||
notify();
|
||||
}
|
||||
|
||||
std::shared_ptr<TrackPlayer> SpircHandler::getTrackPlayer() {
|
||||
return this->trackPlayer;
|
||||
}
|
||||
|
||||
void SpircHandler::sendCmd(MessageType typ) {
|
||||
// Serialize current player state
|
||||
auto encodedFrame = playbackState.encodeCurrentFrame(typ);
|
||||
|
||||
auto responseLambda = [=](MercurySession::Response& res) {
|
||||
};
|
||||
auto parts = MercurySession::DataParts({encodedFrame});
|
||||
ctx->session->execute(MercurySession::RequestType::SEND,
|
||||
"hm://remote/user/" + ctx->config.username + "/",
|
||||
responseLambda, parts);
|
||||
}
|
||||
void SpircHandler::setEventHandler(EventHandler handler) {
|
||||
this->eventHandler = handler;
|
||||
}
|
||||
|
||||
void SpircHandler::setPause(bool isPaused) {
|
||||
if (isPaused) {
|
||||
CSPOT_LOG(debug, "External pause command");
|
||||
playbackState.setPlaybackState(PlaybackState::State::Paused);
|
||||
} else {
|
||||
CSPOT_LOG(debug, "External play command");
|
||||
|
||||
playbackState.setPlaybackState(PlaybackState::State::Playing);
|
||||
}
|
||||
notify();
|
||||
sendEvent(EventType::PLAY_PAUSE, isPaused);
|
||||
}
|
||||
|
||||
void SpircHandler::sendEvent(EventType type) {
|
||||
auto event = std::make_unique<Event>();
|
||||
event->eventType = type;
|
||||
event->data = {};
|
||||
eventHandler(std::move(event));
|
||||
}
|
||||
|
||||
void SpircHandler::sendEvent(EventType type, EventData data) {
|
||||
auto event = std::make_unique<Event>();
|
||||
event->eventType = type;
|
||||
event->data = data;
|
||||
eventHandler(std::move(event));
|
||||
}
|
||||
@@ -1,229 +0,0 @@
|
||||
#include "SpotifyTrack.h"
|
||||
#ifndef _WIN32
|
||||
#include "unistd.h"
|
||||
#endif
|
||||
#include "MercuryManager.h"
|
||||
#include <cassert>
|
||||
#include "CspotAssert.h"
|
||||
#include "Logger.h"
|
||||
#include "ConfigJSON.h"
|
||||
|
||||
SpotifyTrack::SpotifyTrack(std::shared_ptr<MercuryManager> manager, std::shared_ptr<TrackReference> trackReference, uint32_t position_ms, bool isPaused)
|
||||
{
|
||||
this->manager = manager;
|
||||
this->fileId = std::vector<uint8_t>();
|
||||
episodeInfo = {};
|
||||
trackInfo = {};
|
||||
|
||||
mercuryCallback trackResponseLambda = [=](std::unique_ptr<MercuryResponse> res) {
|
||||
this->trackInformationCallback(std::move(res), position_ms, isPaused);
|
||||
};
|
||||
|
||||
mercuryCallback episodeResponseLambda = [=](std::unique_ptr<MercuryResponse> res) {
|
||||
this->episodeInformationCallback(std::move(res), position_ms, isPaused);
|
||||
};
|
||||
|
||||
if (trackReference->isEpisode)
|
||||
{
|
||||
this->reqSeqNum = this->manager->execute(MercuryType::GET, "hm://metadata/3/episode/" + bytesToHexString(trackReference->gid), episodeResponseLambda);
|
||||
}
|
||||
else
|
||||
{
|
||||
this->reqSeqNum = this->manager->execute(MercuryType::GET, "hm://metadata/3/track/" + bytesToHexString(trackReference->gid), trackResponseLambda);
|
||||
}
|
||||
}
|
||||
|
||||
SpotifyTrack::~SpotifyTrack()
|
||||
{
|
||||
this->manager->unregisterMercuryCallback(this->reqSeqNum);
|
||||
this->manager->freeAudioKeyCallback();
|
||||
pb_release(Track_fields, &this->trackInfo);
|
||||
pb_release(Episode_fields, &this->episodeInfo);
|
||||
}
|
||||
|
||||
bool SpotifyTrack::countryListContains(char *countryList, char *country)
|
||||
{
|
||||
uint16_t countryList_length = strlen(countryList);
|
||||
for (int x = 0; x < countryList_length; x += 2)
|
||||
{
|
||||
if (countryList[x] == country[0] && countryList[x + 1] == country[1])
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SpotifyTrack::canPlayTrack(int altIndex)
|
||||
{
|
||||
if(altIndex < 0)
|
||||
{
|
||||
for (int x = 0; x < trackInfo.restriction_count; x++)
|
||||
{
|
||||
if (trackInfo.restriction[x].countries_allowed != nullptr)
|
||||
{
|
||||
return countryListContains(trackInfo.restriction[x].countries_allowed, manager->countryCode);
|
||||
}
|
||||
|
||||
if (trackInfo.restriction[x].countries_forbidden != nullptr)
|
||||
{
|
||||
return !countryListContains(trackInfo.restriction[x].countries_forbidden, manager->countryCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int x = 0; x < trackInfo.alternative[altIndex].restriction_count; x++)
|
||||
{
|
||||
if (trackInfo.alternative[altIndex].restriction[x].countries_allowed != nullptr)
|
||||
{
|
||||
return countryListContains(trackInfo.alternative[altIndex].restriction[x].countries_allowed, manager->countryCode);
|
||||
}
|
||||
|
||||
if (trackInfo.alternative[altIndex].restriction[x].countries_forbidden != nullptr)
|
||||
{
|
||||
return !countryListContains(trackInfo.alternative[altIndex].restriction[x].countries_forbidden, manager->countryCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void SpotifyTrack::trackInformationCallback(std::unique_ptr<MercuryResponse> response, uint32_t position_ms, bool isPaused)
|
||||
{
|
||||
if (this->fileId.size() != 0)
|
||||
return;
|
||||
CSPOT_ASSERT(response->parts.size() > 0, "response->parts.size() must be greater than 0");
|
||||
|
||||
pb_release(Track_fields, &trackInfo);
|
||||
pbDecode(trackInfo, Track_fields, response->parts[0]);
|
||||
|
||||
CSPOT_LOG(info, "Track name: %s", trackInfo.name);
|
||||
CSPOT_LOG(info, "Track duration: %d", trackInfo.duration);
|
||||
CSPOT_LOG(debug, "trackInfo.restriction.size() = %d", trackInfo.restriction_count);
|
||||
|
||||
int altIndex = -1;
|
||||
while (!canPlayTrack(altIndex))
|
||||
{
|
||||
altIndex++;
|
||||
CSPOT_LOG(info, "Trying alternative %d", altIndex);
|
||||
|
||||
if(altIndex >= trackInfo.alternative_count) {
|
||||
// no alternatives for song
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<uint8_t> trackId;
|
||||
this->fileId = std::vector<uint8_t>();
|
||||
|
||||
if(altIndex < 0)
|
||||
{
|
||||
trackId = pbArrayToVector(trackInfo.gid);
|
||||
for (int x = 0; x < trackInfo.file_count; x++)
|
||||
{
|
||||
if (trackInfo.file[x].format == configMan->format)
|
||||
{
|
||||
this->fileId = pbArrayToVector(trackInfo.file[x].file_id);
|
||||
break; // If file found stop searching
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
trackId = pbArrayToVector(trackInfo.alternative[altIndex].gid);
|
||||
for (int x = 0; x < trackInfo.alternative[altIndex].file_count; x++)
|
||||
{
|
||||
if (trackInfo.alternative[altIndex].file[x].format == configMan->format)
|
||||
{
|
||||
this->fileId = pbArrayToVector(trackInfo.alternative[altIndex].file[x].file_id);
|
||||
break; // If file found stop searching
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (trackInfoReceived != nullptr)
|
||||
{
|
||||
auto imageId = pbArrayToVector(trackInfo.album.cover_group.image[0].file_id);
|
||||
TrackInfo simpleTrackInfo = {
|
||||
.name = std::string(trackInfo.name),
|
||||
.album = std::string(trackInfo.album.name),
|
||||
.artist = std::string(trackInfo.artist[0].name),
|
||||
.imageUrl = "https://i.scdn.co/image/" + bytesToHexString(imageId),
|
||||
.duration = trackInfo.duration,
|
||||
|
||||
};
|
||||
|
||||
trackInfoReceived(simpleTrackInfo);
|
||||
}
|
||||
|
||||
this->requestAudioKey(this->fileId, trackId, trackInfo.duration, position_ms, isPaused);
|
||||
}
|
||||
|
||||
void SpotifyTrack::episodeInformationCallback(std::unique_ptr<MercuryResponse> response, uint32_t position_ms, bool isPaused)
|
||||
{
|
||||
if (this->fileId.size() != 0)
|
||||
return;
|
||||
CSPOT_LOG(debug, "Got to episode");
|
||||
CSPOT_ASSERT(response->parts.size() > 0, "response->parts.size() must be greater than 0");
|
||||
pb_release(Episode_fields, &episodeInfo);
|
||||
pbDecode(episodeInfo, Episode_fields, response->parts[0]);
|
||||
|
||||
CSPOT_LOG(info, "--- Episode name: %s", episodeInfo.name);
|
||||
|
||||
this->fileId = std::vector<uint8_t>();
|
||||
|
||||
// TODO: option to set file quality
|
||||
for (int x = 0; x < episodeInfo.audio_count; x++)
|
||||
{
|
||||
if (episodeInfo.audio[x].format == AudioFormat_OGG_VORBIS_96)
|
||||
{
|
||||
this->fileId = pbArrayToVector(episodeInfo.audio[x].file_id);
|
||||
break; // If file found stop searching
|
||||
}
|
||||
}
|
||||
|
||||
if (trackInfoReceived != nullptr)
|
||||
{
|
||||
auto imageId = pbArrayToVector(episodeInfo.covers->image[0].file_id);
|
||||
TrackInfo simpleTrackInfo = {
|
||||
.name = std::string(episodeInfo.name),
|
||||
.album = "",
|
||||
.artist = "",
|
||||
.imageUrl = "https://i.scdn.co/image/" + bytesToHexString(imageId),
|
||||
.duration = trackInfo.duration,
|
||||
|
||||
};
|
||||
|
||||
trackInfoReceived(simpleTrackInfo);
|
||||
}
|
||||
|
||||
this->requestAudioKey(pbArrayToVector(episodeInfo.gid), this->fileId, episodeInfo.duration, position_ms, isPaused);
|
||||
}
|
||||
|
||||
void SpotifyTrack::requestAudioKey(std::vector<uint8_t> fileId, std::vector<uint8_t> trackId, int32_t trackDuration, uint32_t position_ms, bool isPaused)
|
||||
{
|
||||
audioKeyCallback audioKeyLambda = [=](bool success, std::vector<uint8_t> res) {
|
||||
if (success)
|
||||
{
|
||||
CSPOT_LOG(info, "Successfully got audio key!");
|
||||
auto audioKey = std::vector<uint8_t>(res.begin() + 4, res.end());
|
||||
if (this->fileId.size() > 0)
|
||||
{
|
||||
this->audioStream = std::make_unique<ChunkedAudioStream>(this->fileId, audioKey, trackDuration, this->manager, position_ms, isPaused);
|
||||
loadedTrackCallback();
|
||||
}
|
||||
else
|
||||
{
|
||||
CSPOT_LOG(error, "Error while fetching audiokey...");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
auto code = ntohs(extract<uint16_t>(res, 4));
|
||||
CSPOT_LOG(error, "Error while fetching audiokey, error code: %d", code);
|
||||
}
|
||||
};
|
||||
|
||||
this->manager->requestAudioKey(trackId, fileId, audioKeyLambda);
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
#include "TimeProvider.h"
|
||||
#include "Utils.h"
|
||||
#include "Logger.h"
|
||||
|
||||
using namespace cspot;
|
||||
|
||||
TimeProvider::TimeProvider() {
|
||||
}
|
||||
|
||||
void TimeProvider::syncWithPingPacket(const std::vector<uint8_t>& pongPacket) {
|
||||
CSPOT_LOG(debug, "Time synced with spotify servers");
|
||||
// Spotify's timestamp is in seconds since unix time - convert to millis.
|
||||
uint64_t remoteTimestamp = ((uint64_t) ntohl(extract<uint32_t>(pongPacket, 0))) * 1000;
|
||||
this->timestampDiff = remoteTimestamp - getCurrentTimestamp();
|
||||
|
||||
225
components/spotify/cspot/src/TrackPlayer.cpp
Normal file
225
components/spotify/cspot/src/TrackPlayer.cpp
Normal file
@@ -0,0 +1,225 @@
|
||||
#include "TrackPlayer.h"
|
||||
#include <cstddef>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
#include "CDNTrackStream.h"
|
||||
#include "Logger.h"
|
||||
#include "TrackReference.h"
|
||||
|
||||
using namespace cspot;
|
||||
|
||||
static size_t vorbisReadCb(void* ptr, size_t size, size_t nmemb,
|
||||
TrackPlayer* self) {
|
||||
return self->_vorbisRead(ptr, size, nmemb);
|
||||
}
|
||||
|
||||
static int vorbisCloseCb(TrackPlayer* self) {
|
||||
return self->_vorbisClose();
|
||||
}
|
||||
|
||||
static int vorbisSeekCb(TrackPlayer* self, int64_t offset, int whence) {
|
||||
|
||||
return self->_vorbisSeek(offset, whence);
|
||||
}
|
||||
|
||||
static long vorbisTellCb(TrackPlayer* self) {
|
||||
return self->_vorbisTell();
|
||||
}
|
||||
|
||||
TrackPlayer::TrackPlayer(std::shared_ptr<cspot::Context> ctx, isAiringCallback isAiring, EOFCallback eof, TrackLoadedCallback trackLoaded)
|
||||
: bell::Task("cspot_player", 48 * 1024, 5, 1) {
|
||||
this->ctx = ctx;
|
||||
this->isAiring = isAiring;
|
||||
this->eofCallback = eof;
|
||||
this->trackLoaded = trackLoaded;
|
||||
this->trackProvider = std::make_shared<cspot::TrackProvider>(ctx);
|
||||
this->playbackSemaphore = std::make_unique<bell::WrappedSemaphore>(5);
|
||||
|
||||
// Initialize vorbis callbacks
|
||||
vorbisFile = {};
|
||||
vorbisCallbacks = {
|
||||
(decltype(ov_callbacks::read_func))&vorbisReadCb,
|
||||
(decltype(ov_callbacks::seek_func))&vorbisSeekCb,
|
||||
(decltype(ov_callbacks::close_func))&vorbisCloseCb,
|
||||
(decltype(ov_callbacks::tell_func))&vorbisTellCb,
|
||||
};
|
||||
isRunning = true;
|
||||
|
||||
startTask();
|
||||
}
|
||||
|
||||
TrackPlayer::~TrackPlayer() {
|
||||
isRunning = false;
|
||||
std::scoped_lock lock(runningMutex);
|
||||
}
|
||||
|
||||
void TrackPlayer::loadTrackFromRef(TrackReference& ref, size_t positionMs,
|
||||
bool startAutomatically) {
|
||||
this->playbackPosition = positionMs;
|
||||
this->autoStart = startAutomatically;
|
||||
|
||||
auto nextTrack = trackProvider->loadFromTrackRef(ref);
|
||||
|
||||
stopTrack();
|
||||
this->sequence++;
|
||||
this->currentTrackStream = nextTrack;
|
||||
this->playbackSemaphore->give();
|
||||
}
|
||||
|
||||
void TrackPlayer::stopTrack() {
|
||||
this->currentSongPlaying = false;
|
||||
std::scoped_lock lock(playbackMutex);
|
||||
}
|
||||
|
||||
void TrackPlayer::seekMs(size_t ms) {
|
||||
std::scoped_lock lock(seekMutex);
|
||||
#ifdef BELL_VORBIS_FLOAT
|
||||
ov_time_seek(&vorbisFile, (double)ms / 1000);
|
||||
#else
|
||||
ov_time_seek(&vorbisFile, ms);
|
||||
#endif
|
||||
}
|
||||
|
||||
void TrackPlayer::runTask() {
|
||||
std::scoped_lock lock(runningMutex);
|
||||
|
||||
while (isRunning) {
|
||||
this->playbackSemaphore->twait(100);
|
||||
|
||||
if (this->currentTrackStream == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
CSPOT_LOG(info, "Player received a track, waiting for it to be ready...");
|
||||
|
||||
// when track changed many times and very quickly, we are stuck on never-given semaphore
|
||||
while (this->currentTrackStream->trackReady->twait(250));
|
||||
CSPOT_LOG(info, "Got track");
|
||||
|
||||
if (this->currentTrackStream->status == CDNTrackStream::Status::FAILED) {
|
||||
CSPOT_LOG(error, "Track failed to load, skipping it");
|
||||
this->currentTrackStream = nullptr;
|
||||
this->eofCallback();
|
||||
continue;
|
||||
}
|
||||
|
||||
this->currentSongPlaying = true;
|
||||
|
||||
this->trackLoaded();
|
||||
|
||||
this->playbackMutex.lock();
|
||||
|
||||
int32_t r = ov_open_callbacks(this, &vorbisFile, NULL, 0, vorbisCallbacks);
|
||||
|
||||
if (playbackPosition > 0) {
|
||||
#ifdef BELL_VORBIS_FLOAT
|
||||
ov_time_seek(&vorbisFile, (double)playbackPosition / 1000);
|
||||
#else
|
||||
ov_time_seek(&vorbisFile, playbackPosition);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool eof = false;
|
||||
|
||||
while (!eof && currentSongPlaying) {
|
||||
seekMutex.lock();
|
||||
#ifdef BELL_VORBIS_FLOAT
|
||||
long ret = ov_read(&vorbisFile, (char*)&pcmBuffer[0], pcmBuffer.size(),
|
||||
0, 2, 1, ¤tSection);
|
||||
#else
|
||||
long ret = ov_read(&vorbisFile, (char*)&pcmBuffer[0], pcmBuffer.size(),
|
||||
¤tSection);
|
||||
#endif
|
||||
seekMutex.unlock();
|
||||
if (ret == 0) {
|
||||
CSPOT_LOG(info, "EOF");
|
||||
// and done :)
|
||||
eof = true;
|
||||
} else if (ret < 0) {
|
||||
CSPOT_LOG(error, "An error has occured in the stream %d", ret);
|
||||
currentSongPlaying = false;
|
||||
} else {
|
||||
|
||||
if (this->dataCallback != nullptr) {
|
||||
auto toWrite = ret;
|
||||
|
||||
while (!eof && currentSongPlaying && toWrite > 0) {
|
||||
auto written =
|
||||
dataCallback(pcmBuffer.data() + (ret - toWrite), toWrite,
|
||||
this->currentTrackStream->trackInfo.trackId, this->sequence);
|
||||
if (written == 0) {
|
||||
BELL_SLEEP_MS(10);
|
||||
}
|
||||
toWrite -= written;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ov_clear(&vorbisFile);
|
||||
|
||||
// With very large buffers, track N+1 can be downloaded while N has not aired yet and
|
||||
// if we continue, the currentTrackStream will be emptied, causing a crash in
|
||||
// notifyAudioReachedPlayback when it will look for trackInfo. A busy loop is never
|
||||
// ideal, but this low impact, infrequent and more simple than yet another semaphore
|
||||
while (currentSongPlaying && !isAiring()) {
|
||||
BELL_SLEEP_MS(100);
|
||||
}
|
||||
|
||||
// always move back to LOADING (ensure proper seeking after last track has been loaded)
|
||||
this->currentTrackStream.reset();
|
||||
this->playbackMutex.unlock();
|
||||
|
||||
if (eof) {
|
||||
this->eofCallback();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
size_t TrackPlayer::_vorbisRead(void* ptr, size_t size, size_t nmemb) {
|
||||
if (this->currentTrackStream == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
return this->currentTrackStream->readBytes((uint8_t*)ptr, nmemb * size);
|
||||
}
|
||||
|
||||
size_t TrackPlayer::_vorbisClose() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int TrackPlayer::_vorbisSeek(int64_t offset, int whence) {
|
||||
if (this->currentTrackStream == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
switch (whence) {
|
||||
case 0:
|
||||
this->currentTrackStream->seek(offset); // Spotify header offset
|
||||
break;
|
||||
case 1:
|
||||
this->currentTrackStream->seek(this->currentTrackStream->getPosition() +
|
||||
offset);
|
||||
break;
|
||||
case 2:
|
||||
this->currentTrackStream->seek(this->currentTrackStream->getSize() +
|
||||
offset);
|
||||
break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
long TrackPlayer::_vorbisTell() {
|
||||
if (this->currentTrackStream == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
return this->currentTrackStream->getPosition();
|
||||
}
|
||||
|
||||
CDNTrackStream::TrackInfo TrackPlayer::getCurrentTrackInfo() {
|
||||
return this->currentTrackStream->trackInfo;
|
||||
}
|
||||
|
||||
void TrackPlayer::setDataCallback(DataCallback callback) {
|
||||
this->dataCallback = callback;
|
||||
}
|
||||
184
components/spotify/cspot/src/TrackProvider.cpp
Normal file
184
components/spotify/cspot/src/TrackProvider.cpp
Normal file
@@ -0,0 +1,184 @@
|
||||
#include "TrackProvider.h"
|
||||
#include <memory>
|
||||
#include "AccessKeyFetcher.h"
|
||||
#include "CDNTrackStream.h"
|
||||
#include "Logger.h"
|
||||
#include "MercurySession.h"
|
||||
#include "TrackReference.h"
|
||||
#include "Utils.h"
|
||||
#include "protobuf/metadata.pb.h"
|
||||
|
||||
using namespace cspot;
|
||||
|
||||
TrackProvider::TrackProvider(std::shared_ptr<cspot::Context> ctx) {
|
||||
this->accessKeyFetcher = std::make_shared<cspot::AccessKeyFetcher>(ctx);
|
||||
this->ctx = ctx;
|
||||
this->cdnStream =
|
||||
std::make_unique<cspot::CDNTrackStream>(this->accessKeyFetcher);
|
||||
|
||||
this->trackInfo = {};
|
||||
}
|
||||
|
||||
TrackProvider::~TrackProvider() {
|
||||
pb_release(Track_fields, &trackInfo);
|
||||
}
|
||||
|
||||
std::shared_ptr<cspot::CDNTrackStream> TrackProvider::loadFromTrackRef(TrackReference& trackRef) {
|
||||
auto track = std::make_shared<cspot::CDNTrackStream>(this->accessKeyFetcher);
|
||||
this->currentTrackReference = track;
|
||||
this->trackIdInfo = trackRef;
|
||||
|
||||
queryMetadata();
|
||||
return track;
|
||||
}
|
||||
|
||||
void TrackProvider::queryMetadata() {
|
||||
std::string requestUrl = string_format(
|
||||
"hm://metadata/3/%s/%s", trackIdInfo.type == TrackReference::Type::TRACK ? "track" : "episode",
|
||||
bytesToHexString(trackIdInfo.gid).c_str());
|
||||
CSPOT_LOG(debug, "Requesting track metadata from %s", requestUrl.c_str());
|
||||
|
||||
auto responseHandler = [this](MercurySession::Response& res) {
|
||||
this->onMetadataResponse(res);
|
||||
};
|
||||
|
||||
// Execute the request
|
||||
ctx->session->execute(MercurySession::RequestType::GET, requestUrl,
|
||||
responseHandler);
|
||||
}
|
||||
|
||||
void TrackProvider::onMetadataResponse(MercurySession::Response& res) {
|
||||
CSPOT_LOG(debug, "Got track metadata response");
|
||||
|
||||
pb_release(Track_fields, &trackInfo);
|
||||
pbDecode(trackInfo, Track_fields, res.parts[0]);
|
||||
|
||||
CSPOT_LOG(info, "Track name: %s", trackInfo.name);
|
||||
CSPOT_LOG(info, "Track duration: %d", trackInfo.duration);
|
||||
|
||||
CSPOT_LOG(debug, "trackInfo.restriction.size() = %d",
|
||||
trackInfo.restriction_count);
|
||||
|
||||
int altIndex = -1;
|
||||
while (!canPlayTrack(altIndex)) {
|
||||
altIndex++;
|
||||
CSPOT_LOG(info, "Trying alternative %d", altIndex);
|
||||
|
||||
if (altIndex >= trackInfo.alternative_count) {
|
||||
// no alternatives for song
|
||||
if (!this->currentTrackReference.expired()) {
|
||||
auto trackRef = this->currentTrackReference.lock();
|
||||
trackRef->status = CDNTrackStream::Status::FAILED;
|
||||
trackRef->trackReady->give();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<uint8_t> trackId;
|
||||
std::vector<uint8_t> fileId;
|
||||
AudioFormat format = AudioFormat_OGG_VORBIS_160;
|
||||
|
||||
if (altIndex < 0) {
|
||||
trackId = pbArrayToVector(trackInfo.gid);
|
||||
for (int x = 0; x < trackInfo.file_count; x++) {
|
||||
if (trackInfo.file[x].format == format) {
|
||||
fileId = pbArrayToVector(trackInfo.file[x].file_id);
|
||||
break; // If file found stop searching
|
||||
}
|
||||
}
|
||||
} else {
|
||||
trackId = pbArrayToVector(trackInfo.alternative[altIndex].gid);
|
||||
for (int x = 0; x < trackInfo.alternative[altIndex].file_count; x++) {
|
||||
if (trackInfo.alternative[altIndex].file[x].format == format) {
|
||||
fileId =
|
||||
pbArrayToVector(trackInfo.alternative[altIndex].file[x].file_id);
|
||||
break; // If file found stop searching
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!this->currentTrackReference.expired()) {
|
||||
auto trackRef = this->currentTrackReference.lock();
|
||||
|
||||
auto imageId =
|
||||
pbArrayToVector(trackInfo.album.cover_group.image[0].file_id);
|
||||
|
||||
trackRef->trackInfo.trackId = bytesToHexString(trackIdInfo.gid);
|
||||
trackRef->trackInfo.name = std::string(trackInfo.name);
|
||||
trackRef->trackInfo.album = std::string(trackInfo.album.name);
|
||||
trackRef->trackInfo.artist = std::string(trackInfo.artist[0].name);
|
||||
trackRef->trackInfo.imageUrl =
|
||||
"https://i.scdn.co/image/" + bytesToHexString(imageId);
|
||||
trackRef->trackInfo.duration = trackInfo.duration;
|
||||
}
|
||||
|
||||
this->fetchFile(fileId, trackId);
|
||||
}
|
||||
|
||||
void TrackProvider::fetchFile(const std::vector<uint8_t>& fileId,
|
||||
const std::vector<uint8_t>& trackId) {
|
||||
ctx->session->requestAudioKey(
|
||||
trackId, fileId,
|
||||
[this, fileId](bool success, const std::vector<uint8_t>& audioKey) {
|
||||
if (success) {
|
||||
CSPOT_LOG(info, "Got audio key");
|
||||
if (!this->currentTrackReference.expired()) {
|
||||
auto ref = this->currentTrackReference.lock();
|
||||
ref->fetchFile(fileId, audioKey);
|
||||
}
|
||||
|
||||
} else {
|
||||
CSPOT_LOG(error, "Failed to get audio key");
|
||||
if (!this->currentTrackReference.expired()) {
|
||||
auto ref = this->currentTrackReference.lock();
|
||||
ref->fail();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool countryListContains(char* countryList, char* country) {
|
||||
uint16_t countryList_length = strlen(countryList);
|
||||
for (int x = 0; x < countryList_length; x += 2) {
|
||||
if (countryList[x] == country[0] && countryList[x + 1] == country[1]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TrackProvider::canPlayTrack(int altIndex) {
|
||||
if (altIndex < 0) {
|
||||
for (int x = 0; x < trackInfo.restriction_count; x++) {
|
||||
if (trackInfo.restriction[x].countries_allowed != nullptr) {
|
||||
return countryListContains(trackInfo.restriction[x].countries_allowed,
|
||||
(char*)ctx->config.countryCode.c_str());
|
||||
}
|
||||
|
||||
if (trackInfo.restriction[x].countries_forbidden != nullptr) {
|
||||
return !countryListContains(
|
||||
trackInfo.restriction[x].countries_forbidden,
|
||||
(char*)ctx->config.countryCode.c_str());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (int x = 0; x < trackInfo.alternative[altIndex].restriction_count;
|
||||
x++) {
|
||||
if (trackInfo.alternative[altIndex].restriction[x].countries_allowed !=
|
||||
nullptr) {
|
||||
return countryListContains(
|
||||
trackInfo.alternative[altIndex].restriction[x].countries_allowed,
|
||||
(char*)ctx->config.countryCode.c_str());
|
||||
}
|
||||
|
||||
if (trackInfo.alternative[altIndex].restriction[x].countries_forbidden !=
|
||||
nullptr) {
|
||||
return !countryListContains(
|
||||
trackInfo.alternative[altIndex].restriction[x].countries_forbidden,
|
||||
(char*)ctx->config.countryCode.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
#include "TrackReference.h"
|
||||
#include "Logger.h"
|
||||
|
||||
TrackReference::TrackReference(TrackRef *ref)
|
||||
{
|
||||
if (ref->gid != nullptr)
|
||||
{
|
||||
gid = pbArrayToVector(ref->gid);
|
||||
}
|
||||
else if (ref->uri != nullptr)
|
||||
{
|
||||
auto uri = std::string(ref->uri);
|
||||
auto idString = uri.substr(uri.find_last_of(":") + 1, uri.size());
|
||||
CSPOT_LOG(debug, "idString = %s", idString.c_str());
|
||||
gid = base62Decode(idString);
|
||||
isEpisode = true;
|
||||
}
|
||||
}
|
||||
|
||||
TrackReference::~TrackReference()
|
||||
{
|
||||
//pb_release(TrackRef_fields, &ref);
|
||||
//pbFree(TrackRef_fields, &ref);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> TrackReference::base62Decode(std::string uri)
|
||||
{
|
||||
std::vector<uint8_t> n = std::vector<uint8_t>({0});
|
||||
|
||||
for (int x = 0; x < uri.size(); x++)
|
||||
{
|
||||
size_t d = alphabet.find(uri[x]);
|
||||
n = bigNumMultiply(n, 62);
|
||||
n = bigNumAdd(n, d);
|
||||
}
|
||||
|
||||
return n;
|
||||
}
|
||||
@@ -23,7 +23,20 @@ uint64_t hton64(uint64_t value) {
|
||||
}
|
||||
}
|
||||
|
||||
std::string bytesToHexString(std::vector<uint8_t>& v) {
|
||||
std::vector<uint8_t> stringHexToBytes(const std::string & s) {
|
||||
std::vector<uint8_t> v;
|
||||
v.reserve(s.length() / 2);
|
||||
|
||||
for (std::string::size_type i = 0; i < s.length(); i += 2) {
|
||||
std::string byteString = s.substr(i, 2);
|
||||
uint8_t byte = (uint8_t) strtol(byteString.c_str(), NULL, 16);
|
||||
v.push_back(byte);
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
std::string bytesToHexString(const std::vector<uint8_t>& v) {
|
||||
std::stringstream ss;
|
||||
ss << std::hex << std::setfill('0');
|
||||
std::vector<uint8_t>::const_iterator it;
|
||||
@@ -64,6 +77,28 @@ std::vector<uint8_t> bigNumAdd(std::vector<uint8_t> num, int n)
|
||||
return num;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> bigNumDivide(std::vector<uint8_t> num, int n)
|
||||
{
|
||||
auto carry = 0;
|
||||
for (int x = 0; x < num.size(); x++)
|
||||
{
|
||||
int res = num[x] + carry * 256;
|
||||
if (res < n)
|
||||
{
|
||||
carry = res;
|
||||
num[x] = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Carry the rest of the division
|
||||
carry = res % n;
|
||||
num[x] = res / n;
|
||||
}
|
||||
}
|
||||
|
||||
return num;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> bigNumMultiply(std::vector<uint8_t> num, int n)
|
||||
{
|
||||
auto carry = 0;
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
#include "ZeroconfAuthenticator.h"
|
||||
#include "JSONObject.h"
|
||||
#include <sstream>
|
||||
#ifndef _WIN32
|
||||
#include <sys/select.h>
|
||||
#else
|
||||
#include <iphlpapi.h>
|
||||
#pragma comment(lib, "IPHLPAPI.lib")
|
||||
#endif
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include "Logger.h"
|
||||
#include "CspotAssert.h"
|
||||
#include "ConfigJSON.h"
|
||||
|
||||
// provide weak deviceId (see ConstantParameters.h)
|
||||
#if _MSC_VER
|
||||
char deviceId[] = "142137fd329622137a14901634264e6f332e2411";
|
||||
#else
|
||||
char deviceId[] __attribute__((weak)) = "142137fd329622137a14901634264e6f332e2411";
|
||||
#endif
|
||||
|
||||
ZeroconfAuthenticator::ZeroconfAuthenticator(authCallback callback, std::shared_ptr<bell::BaseHTTPServer> httpServer) {
|
||||
this->gotBlobCallback = callback;
|
||||
srand((unsigned int)time(NULL));
|
||||
|
||||
this->crypto = std::make_unique<Crypto>();
|
||||
this->crypto->dhInit();
|
||||
this->server = httpServer;
|
||||
|
||||
#ifdef _WIN32
|
||||
char hostname[128];
|
||||
gethostname(hostname, sizeof(hostname));
|
||||
|
||||
struct sockaddr_in* host = NULL;
|
||||
ULONG size = sizeof(IP_ADAPTER_ADDRESSES) * 32;
|
||||
IP_ADAPTER_ADDRESSES* adapters = (IP_ADAPTER_ADDRESSES*) malloc(size);
|
||||
int ret = GetAdaptersAddresses(AF_UNSPEC, GAA_FLAG_INCLUDE_GATEWAYS | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_ANYCAST, 0, adapters, &size);
|
||||
|
||||
for (PIP_ADAPTER_ADDRESSES adapter = adapters; adapter && !host; adapter = adapter->Next) {
|
||||
if (adapter->TunnelType == TUNNEL_TYPE_TEREDO) continue;
|
||||
if (adapter->OperStatus != IfOperStatusUp) continue;
|
||||
|
||||
for (IP_ADAPTER_UNICAST_ADDRESS* unicast = adapter->FirstUnicastAddress; unicast;
|
||||
unicast = unicast->Next) {
|
||||
if (adapter->FirstGatewayAddress && unicast->Address.lpSockaddr->sa_family == AF_INET) {
|
||||
host = (struct sockaddr_in*)unicast->Address.lpSockaddr;
|
||||
BELL_LOG(info, "mdns", "mDNS on interface %s", inet_ntoa(host->sin_addr));
|
||||
this->service = mdnsd_start(host->sin_addr, false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CSPOT_ASSERT(this->service, "can't start mDNS service");
|
||||
mdnsd_set_hostname(this->service, hostname, host->sin_addr);
|
||||
#endif
|
||||
}
|
||||
|
||||
void ZeroconfAuthenticator::registerHandlers() {
|
||||
// Make it discoverable for spoti clients
|
||||
registerZeroconf();
|
||||
auto getInfoHandler = [this](std::unique_ptr<bell::HTTPRequest> request) {
|
||||
CSPOT_LOG(info, "Got request for info");
|
||||
bell::HTTPResponse response = {
|
||||
.connectionFd = request->connection,
|
||||
.status = 200,
|
||||
.body = this->buildJsonInfo(),
|
||||
.contentType = "application/json",
|
||||
};
|
||||
server->respond(response);
|
||||
};
|
||||
|
||||
auto addUserHandler = [this](std::unique_ptr<bell::HTTPRequest> request) {
|
||||
BELL_LOG(info, "http", "Got request for adding user");
|
||||
bell::JSONObject obj;
|
||||
obj["status"] = 101;
|
||||
obj["spotifyError"] = 0;
|
||||
obj["statusString"] = "ERROR-OK";
|
||||
|
||||
bell::HTTPResponse response = {
|
||||
.connectionFd = request->connection,
|
||||
.status = 200,
|
||||
.body = obj.toString(),
|
||||
.contentType = "application/json",
|
||||
};
|
||||
server->respond(response);
|
||||
|
||||
auto correctBlob = this->getParameterFromUrlEncoded(request->body, "blob");
|
||||
this->handleAddUser(request->queryParams);
|
||||
};
|
||||
|
||||
BELL_LOG(info, "cspot", "Zeroconf registering handlers");
|
||||
this->server->registerHandler(bell::RequestType::GET, "/spotify_info", getInfoHandler);
|
||||
this->server->registerHandler(bell::RequestType::POST, "/spotify_info", addUserHandler);
|
||||
}
|
||||
|
||||
void ZeroconfAuthenticator::registerZeroconf()
|
||||
{
|
||||
const char* service = "_spotify-connect._tcp";
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
mdns_txt_item_t serviceTxtData[3] = {
|
||||
{"VERSION", "1.0"},
|
||||
{"CPath", "/spotify_info"},
|
||||
{"Stack", "SP"} };
|
||||
mdns_service_add("cspot", "_spotify-connect", "_tcp", this->server->serverPort, serviceTxtData, 3);
|
||||
#elif _WIN32
|
||||
const char *serviceTxtData[] = {
|
||||
"VERSION=1.0",
|
||||
"CPath=/spotify_info",
|
||||
"Stack=SP",
|
||||
NULL };
|
||||
mdnsd_register_svc(this->service, "cspot", "_spotify-connect._tcp.local", this->server->serverPort, NULL, serviceTxtData);
|
||||
#else
|
||||
DNSServiceRef ref = NULL;
|
||||
TXTRecordRef txtRecord;
|
||||
TXTRecordCreate(&txtRecord, 0, NULL);
|
||||
TXTRecordSetValue(&txtRecord, "VERSION", 3, "1.0");
|
||||
TXTRecordSetValue(&txtRecord, "CPath", 13, "/spotify_info");
|
||||
TXTRecordSetValue(&txtRecord, "Stack", 2, "SP");
|
||||
DNSServiceRegister(&ref, 0, 0, (char*)informationString, service, NULL, NULL, htons(this->server->serverPort), TXTRecordGetLength(&txtRecord), TXTRecordGetBytesPtr(&txtRecord), NULL, NULL);
|
||||
TXTRecordDeallocate(&txtRecord);
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string ZeroconfAuthenticator::getParameterFromUrlEncoded(std::string data, std::string param)
|
||||
{
|
||||
auto startStr = data.substr(data.find("&" + param + "=") + param.size() + 2, data.size());
|
||||
return urlDecode(startStr.substr(0, startStr.find("&")));
|
||||
}
|
||||
|
||||
void ZeroconfAuthenticator::handleAddUser(std::map<std::string, std::string>& queryData)
|
||||
{
|
||||
// Get all urlencoded params
|
||||
auto username = queryData["userName"];
|
||||
auto blobString = queryData["blob"];
|
||||
auto clientKeyString = queryData["clientKey"];
|
||||
auto deviceName = queryData["deviceName"];
|
||||
|
||||
// client key and bytes are urlencoded
|
||||
auto clientKeyBytes = crypto->base64Decode(clientKeyString);
|
||||
auto blobBytes = crypto->base64Decode(blobString);
|
||||
|
||||
// Generated secret based on earlier generated DH
|
||||
auto secretKey = crypto->dhCalculateShared(clientKeyBytes);
|
||||
|
||||
auto loginBlob = std::make_shared<LoginBlob>();
|
||||
|
||||
std::string deviceIdStr = deviceId;
|
||||
|
||||
loginBlob->loadZeroconf(blobBytes, secretKey, deviceIdStr, username);
|
||||
|
||||
gotBlobCallback(loginBlob);
|
||||
}
|
||||
|
||||
std::string ZeroconfAuthenticator::buildJsonInfo()
|
||||
{
|
||||
// Encode publicKey into base64
|
||||
auto encodedKey = crypto->base64Encode(crypto->publicKey);
|
||||
|
||||
bell::JSONObject obj;
|
||||
obj["status"] = 101;
|
||||
obj["statusString"] = "OK";
|
||||
obj["version"] = protocolVersion;
|
||||
obj["spotifyError"] = 0;
|
||||
obj["libraryVersion"] = swVersion;
|
||||
obj["accountReq"] = "PREMIUM";
|
||||
obj["brandDisplayName"] = brandName;
|
||||
obj["modelDisplayName"] = configMan->deviceName.c_str();
|
||||
obj["voiceSupport"] = "NO";
|
||||
obj["availability"] = "";
|
||||
obj["productID"] = 0;
|
||||
obj["tokenType"] = "default";
|
||||
obj["groupStatus"] = "NONE";
|
||||
obj["resolverVersion"] = "0";
|
||||
obj["scope"] = "streaming,client-authorization-universal";
|
||||
obj["activeUser"] = "";
|
||||
obj["deviceID"] = deviceId;
|
||||
obj["remoteName"] = configMan->deviceName.c_str();
|
||||
obj["publicKey"] = encodedKey;
|
||||
obj["deviceType"] = "SPEAKER";
|
||||
return obj.toString();
|
||||
}
|
||||
Reference in New Issue
Block a user