Add iOS 27 remote media session and system player extension
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>ru</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>MusicBridge</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>EXAppExtensionAttributes</key>
|
||||
<dict>
|
||||
<key>EXExtensionPointIdentifier</key>
|
||||
<string>com.apple.nowplaying.remote-media</string>
|
||||
</dict>
|
||||
<key>MusicBridgeSharedKeychainGroup</key>
|
||||
<string>$(AppIdentifierPrefix)ru.yukinoki.musicbridge.remote-control</string>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>MusicBridge подключается к вашему ПК, чтобы показывать музыку и управлять воспроизведением.</string>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
<true/>
|
||||
<key>NSExceptionDomains</key>
|
||||
<dict>
|
||||
<key>10.0.0.0/8</key>
|
||||
<dict>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>172.16.0.0/12</key>
|
||||
<dict>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>192.168.0.0/16</key>
|
||||
<dict>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,68 @@
|
||||
import ExtensionFoundation
|
||||
import NowPlaying
|
||||
import Observation
|
||||
import Foundation
|
||||
|
||||
@main
|
||||
struct MusicBridgeRemote: RemoteMediaSessionExtension {
|
||||
var configuration: RemoteMediaSessionExtensionConfiguration<Self> {
|
||||
RemoteMediaSessionExtensionConfiguration(extension: self)
|
||||
}
|
||||
func session(_ attributes: SystemPlayerAttributes) async throws -> PCMediaSession {
|
||||
_ = try SystemPlayerCredentials.load(attributes.id)
|
||||
return PCMediaSession(attributes)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor @Observable
|
||||
final class PCMediaSession: RemoteMediaSessionRepresentable {
|
||||
let id: String
|
||||
var attributes: SystemPlayerAttributes
|
||||
private let transport = SystemPlayerTransport()
|
||||
init(_ attributes: SystemPlayerAttributes) { id = attributes.id; self.attributes = attributes }
|
||||
|
||||
func update(_ attributes: SystemPlayerAttributes) { self.attributes = attributes }
|
||||
|
||||
var content: (any MediaContentRepresentable)? {
|
||||
guard attributes.hasSession else { return nil }
|
||||
let artwork = attributes.artworkID.map { artworkID in
|
||||
Artwork(id: artworkID) { _ in
|
||||
let saved = try SystemPlayerCredentials.load(self.id)
|
||||
let client = BridgeClient(saved, timeout: 5)
|
||||
defer { client.close() }
|
||||
return try ArtworkRepresentation(data: await client.artwork(artworkID))
|
||||
}
|
||||
}
|
||||
return MusicContent(id: attributes.source + "/" + attributes.title + "/" + attributes.artist,
|
||||
songTitle: attributes.title.isEmpty ? "Без названия" : attributes.title,
|
||||
artistName: attributes.artist, albumName: "", type: .audio,
|
||||
duration: attributes.duration > 0 ? .finite(attributes.duration) : nil, artwork: artwork)
|
||||
}
|
||||
|
||||
var playbackSnapshot: MediaPlaybackSnapshot? {
|
||||
MediaPlaybackSnapshot(state: attributes.playing ? .playing(rate: attributes.rate) : .paused,
|
||||
elapsedTime: attributes.position, timestamp: attributes.timestamp)
|
||||
}
|
||||
|
||||
var commands: [MediaCommand] {
|
||||
[
|
||||
.play { try await self.send(.play) }.enabled(attributes.capabilities.play),
|
||||
.pause { try await self.send(.pause) }.enabled(attributes.capabilities.pause),
|
||||
.next { try await self.send(.next) }.enabled(attributes.capabilities.next),
|
||||
.previous { try await self.send(.previous) }.enabled(attributes.capabilities.previous),
|
||||
.seekToPosition { try await self.send(.seek, value: $0) }.enabled(attributes.capabilities.seek)
|
||||
]
|
||||
}
|
||||
|
||||
var devices: [MediaDevice] {
|
||||
let capabilities: [MediaDevice.Capability] = attributes.volume.map { volume in
|
||||
[.absoluteVolume(Float(volume)) { try await self.send(.volume, value: Double($0)) }]
|
||||
} ?? []
|
||||
return [MediaDevice(id: id, name: "MusicBridge · ПК", type: .speaker, capabilities: capabilities)]
|
||||
}
|
||||
|
||||
private func send(_ action: SystemPlayerAction, value: Double? = nil) async throws {
|
||||
let state = try await transport.send(action, value: value, sessionID: id)
|
||||
attributes = SystemPlayerAttributes(id: id, state: state)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<string>$(AppIdentifierPrefix)ru.yukinoki.musicbridge.remote-control</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
Reference in New Issue
Block a user