diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fd9c0e..7525ded 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # changelog ## unreleased +- `setClientName()`: the name the receiver shows (Apple TV pairing dialog + the + AP2 source) is no longer hardcoded to "FXChainPlayer", defaults to + "airplay2-sender-cpp". - provenance made precise: the crypto/wire-format core is clean-room; the RAOP/AP2 transport in `raop_sender.cpp` is credited as a C++ port of pyatv (MIT). pyatv's MIT notice now ships in `licenses/THIRD-PARTY-NOTICES.txt`. diff --git a/src/raop_sender.cpp b/src/raop_sender.cpp index c2bf38d..6093fd6 100644 --- a/src/raop_sender.cpp +++ b/src/raop_sender.cpp @@ -468,6 +468,11 @@ void RaopSender::setNowPlaying(const QString& title, const QString& artist, if (state_ == State::Streaming && changed) sendMetadata_(); } +void RaopSender::setClientName(const QString& name) { + clientName_ = name.isEmpty() ? QByteArrayLiteral("airplay2-sender-cpp") + : name.toUtf8(); +} + namespace { // DMAP tag: 4-char ASCII code + 4-byte big-endian length + payload. QByteArray dmapTag(const char code[5], const QByteArray& payload) { @@ -548,7 +553,7 @@ void RaopSender::sendRequest_(const QByteArray& method, const QByteArray& uri, req += "DACP-ID: " + dacpId_ + "\r\n"; req += "Active-Remote: " + QByteArray::number(activeRemote_) + "\r\n"; req += "Client-Instance: " + dacpId_ + "\r\n"; - req += "X-Apple-Client-Name: FXChainPlayer\r\n"; // AirPlay-bug fix, owntone parity + req += "X-Apple-Client-Name: " + clientName_ + "\r\n"; // AirPlay-bug fix, owntone parity // v0.66.x, RTSP digest auth for pw=true receivers: once a 401 has // told us realm+nonce, every subsequent request carries Authorization. if (!digestNonce_.isEmpty() && !digestPassword_.isEmpty()) { @@ -1041,7 +1046,7 @@ void RaopSender::httpPost_(const QByteArray& uri, const QByteArray& contentType, // absence is a documented 403 cause. Mirror owntone (Client-Instance == // DACP-ID value). req += "Client-Instance: " + dacpId_ + "\r\n"; - req += "X-Apple-Client-Name: FXChainPlayer\r\n"; + req += "X-Apple-Client-Name: " + clientName_ + "\r\n"; if (!contentType.isEmpty()) req += "Content-Type: " + contentType + "\r\n"; req += "Content-Length: " + QByteArray::number(body.size()) + "\r\n"; @@ -1602,7 +1607,7 @@ void RaopSender::sendAp2Rtsp_(const QByteArray& method, const QByteArray& uri, req += "DACP-ID: " + dacpId_ + "\r\n"; req += "Active-Remote: " + QByteArray::number(activeRemote_) + "\r\n"; req += "Client-Instance: " + dacpId_ + "\r\n"; - req += "X-Apple-Client-Name: FXChainPlayer\r\n"; + req += "X-Apple-Client-Name: " + clientName_ + "\r\n"; if (method == "SETUP") req += "X-Apple-StreamID: 1\r\n"; // owntone/pyatv parity if (!contentType.isEmpty()) @@ -1635,7 +1640,7 @@ void RaopSender::sendAp2SetupSession_() { d.emplace_back("groupContainsGroupLeader", Value::boolean(false)); d.emplace_back("macAddress", Value::str("AA:BB:CC:DD:EE:FF")); d.emplace_back("model", Value::str("iPhone14,3")); - d.emplace_back("name", Value::str("FXChainPlayer")); + d.emplace_back("name", Value::str(clientName_.toStdString())); d.emplace_back("osBuildVersion", Value::str("20F66")); d.emplace_back("osName", Value::str("iPhone OS")); d.emplace_back("osVersion", Value::str("16.5")); diff --git a/src/raop_sender.h b/src/raop_sender.h index 823d4c0..25c288e 100644 --- a/src/raop_sender.h +++ b/src/raop_sender.h @@ -113,6 +113,9 @@ class RaopSender : public QObject { const QByteArray& cover = {}, const QByteArray& coverMime = {}); + // Sender name the receiver shows (PIN dialog + AirPlay source); empty = default. + void setClientName(const QString& name); + signals: void launched(bool ok, const QString& error); // RECORD accepted / failed void closed(); // session ended @@ -286,6 +289,7 @@ class RaopSender : public QObject { QString npTitle_, npArtist_, npAlbum_; // now-playing metadata QByteArray npCover_, npCoverMime_; // cover art bytes + MIME + QByteArray clientName_ = QByteArrayLiteral("airplay2-sender-cpp"); // X-Apple-Client-Name + AP2 name // ── v0.66.x Phase 2/3, auth + AP2 state ───────────────────────── // The pairing sub-state machine: which reply the next HTTP POST's