You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过编程将本地音频发送至指定AirPlay音箱?

问题描述

我希望将本地MP3文件播放至家中已知的支持AirPlay的音箱。我可以通过编程调出AVRoutePickerView,但更希望获取已知AirPlay音箱列表,通过名称查找目标音箱并编程选择它来播放文件。这仅用于概念验证,不会用于实际发布。我曾尝试破解选择器获取设备列表并触发对应按钮的touchUpInside事件,但一无所获。

2023年3月7日更新

我尝试发现AirPlay设备,希望通过MCPeerID进行连接,但目前还不知道具体方法。当我实现以下类时,我得到了NetService名称列表,但仍不清楚如何连接到目标设备并播放本地MP3文件。

let apd = AirPlaySpeakerDiscovery()
apd.start()

这会在控制台输出服务名称,非常好。现在如何将本地MP3文件路由至其中一个服务?比如"Kitchen HomePod"?

import Foundation
import Network
class AirPlaySpeakerDiscovery: NSObject
{
    private var browser: NetServiceBrowser?
    private var airplayServices = [NetService]()

    override init() {
        super.init()
        self.browser = NetServiceBrowser()
        self.browser?.delegate = self
    }

    func start() {
        self.browser?.searchForServices(ofType: "_airplay._tcp.", inDomain: "local.")
    }

    func stop() {
        self.browser?.stop()
    }

    func printAirPlayServices() {
        for service in self.airplayServices {
            print("Service Name: \(service.name)")
            // How to connect to one of them now?
            if (service.name == "Kitchen HomePod") {
                // Play local mp3 file on it?
            }
        }
    }
}

extension AirPlaySpeakerDiscovery: NetServiceBrowserDelegate {
    func netServiceBrowser(_ browser: NetServiceBrowser, didFind service: NetService, moreComing: Bool) {
        self.airplayServices.append(service)
        if !moreComing {
            self.printAirPlayServices()
        }
    }

    func netServiceBrowser(_ browser: NetServiceBrowser, didRemove service: NetService, moreComing: Bool) {
        if let index = self.airplayServices.firstIndex(of: service) {
            self.airplayServices.remove(at: index)
        }
        if !moreComing {
            self.printAirPlayServices()
        }
    }

    func netServiceBrowser(_ browser: NetServiceBrowser, didNotSearch errorDict: [String : NSNumber]) {
        print("Error searching for AirPlay services: \(errorDict)")
    }
}
解决方案

针对概念验证场景,可以结合NetService解析和AVFoundation框架实现需求,步骤如下:

1. 完善NetService解析,获取设备完整信息

当前代码仅获取了服务名称,需要进一步解析服务的IP、端口和AirPlay设备标识。修改原类,添加服务解析逻辑:

// 在AirPlaySpeakerDiscovery类中添加方法和扩展
func printAirPlayServices() {
    for service in self.airplayServices {
        if service.name == "Kitchen HomePod" {
            // 解析服务的地址和TXT记录
            service.resolve(withTimeout: 5.0)
            service.delegate = self
        }
    }
}

extension AirPlaySpeakerDiscovery: NetServiceDelegate {
    func netServiceDidResolveAddress(_ sender: NetService) {
        guard let addresses = sender.addresses, let firstAddr = addresses.first else {
            print("无法获取设备地址")
            return
        }
        
        // 解析IP和端口
        var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
        var portStr = [CChar](repeating: 0, count: Int(NI_MAXSERV))
        getnameinfo(firstAddr.bytes, socklen_t(firstAddr.count), &hostname, socklen_t(hostname.count), &portStr, socklen_t(portStr.count), NI_NUMERICHOST | NI_NUMERICSERV)
        
        let ip = String(cString: hostname)
        let port = String(cString: portStr)
        
        // 从TXT记录中获取设备唯一标识
        if let txtData = sender.txtRecordData() {
            let txtDict = NetService.dictionary(fromTXTRecord: txtData)
            if let deviceIDData = txtDict[b"deviceid"], let deviceID = String(data: deviceIDData, encoding: .utf8) {
                print("设备信息:IP=\(ip),端口=\(port),DeviceID=\(deviceID)")
                // 触发播放逻辑
                playMP3OnTargetSpeaker(deviceName: sender.name)
            }
        }
    }
    
    func netService(_ sender: NetService, didNotResolve errorDict: [String : NSNumber]) {
        print("解析服务失败:\(errorDict)")
    }
}

2. 用AVFoundation实现路由选择与播放

利用AVAudioSession的路由管理功能,匹配目标设备后播放本地MP3:

import AVFoundation

func playMP3OnTargetSpeaker(deviceName: String) {
    let audioSession = AVAudioSession.sharedInstance()
    do {
        // 配置音频会话为播放模式
        try audioSession.setCategory(.playback, mode: .default)
        try audioSession.activate()
    } catch {
        print("音频会话初始化失败:\(error)")
        return
    }
    
    // 遍历可用输出路由,匹配目标设备
    for route in audioSession.availableOutputRoutes {
        if route.portName == deviceName {
            do {
                // 设置首选输出路由
                try audioSession.setPreferredOutputAudioPort(route.portType)
                // 初始化AVPlayer播放本地MP3
                guard let mp3URL = Bundle.main.url(forResource: "你的MP3文件名", withExtension: "mp3") else {
                    print("找不到本地MP3文件")
                    return
                }
                let player = AVPlayer(url: mp3URL)
                player.play()
            } catch {
                print("设置路由失败:\(error)")
            }
            break
        }
    }
}

注意事项

  • 公开API无法直接通过IP指定AirPlay路由,只能通过设备名称匹配;若需要更直接的控制,可尝试AirPlay私有框架(仅适用于概念验证,违反App Store规则)。
  • 需在Info.plist中添加NSAppleMusicUsageDescription权限描述,确保应用能访问音频资源。
  • 若存在同名设备,建议结合解析到的DeviceID做更精确的匹配。

内容的提问来源于stack exchange,提问作者Eric Dolecki

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 06:40:36