From ea2000af96f4c1a5097e77665c84009f019bd86e Mon Sep 17 00:00:00 2001 From: fanpeng <438123081@qq.com> Date: Fri, 4 Jul 2025 18:53:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20ios=E6=B7=BB=E5=8A=A0=E5=BD=95=E9=9F=B3?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- manifest.json | 216 ++++++++-------- pages/main/lockDetail.vue | 2 +- pages/p2p/p2pPlayer.vue | 57 ++-- .../xhj-record/utssdk/app-ios/Info.plist | 8 + .../xhj-record/utssdk/app-ios/hybrid.swift | 244 ++++++++++++++++++ .../xhj-record/utssdk/app-ios/index.uts | 85 ++++++ .../utssdk/app-android/index.uts | 2 +- .../utssdk/app-ios/hybrid.swift | 25 +- .../xhj-tencent-xp2p/utssdk/app-ios/index.uts | 42 ++- 9 files changed, 541 insertions(+), 140 deletions(-) create mode 100644 uni_modules/xhj-record/utssdk/app-ios/Info.plist create mode 100644 uni_modules/xhj-record/utssdk/app-ios/hybrid.swift diff --git a/manifest.json b/manifest.json index 048b579..22d3396 100644 --- a/manifest.json +++ b/manifest.json @@ -1,114 +1,114 @@ { - "name": "星星锁Lite", - "appid": "__UNI__933D519", - "description": "", - "versionName": "1.3.2", - "versionCode": "39", - "mp-weixin": { - "appid": "wx9829a39e65550757", - "setting": { - "urlCheck": true, - "minified": true - }, - "permission": { - "scope.bluetooth": { - "desc": "蓝牙将用于控制和管理您的智能门锁" - } - }, - "usingComponents": true, - "lazyCodeLoading": "requiredComponents", - "optimization": { - "subPackages": true - }, - "plugins": { - "wmpf-voip": { - "version": "latest", - "provider": "wxf830863afde621eb", - "genericsImplementation": { - "call-page-plugin": { - "custombox": "pages/main/customBox" - } - } - } - } - }, - "vueVersion": "3", - "app-plus": { - "distribute": { - "icons": { - "android": { - "hdpi": "unpackage/res/icons/72x72.png", - "xhdpi": "unpackage/res/icons/96x96.png", - "xxhdpi": "unpackage/res/icons/144x144.png", - "xxxhdpi": "unpackage/res/icons/192x192.png" + "name" : "星星锁Lite", + "appid" : "__UNI__933D519", + "description" : "", + "versionName" : "1.3.2", + "versionCode" : "39", + "mp-weixin" : { + "appid" : "wx9829a39e65550757", + "setting" : { + "urlCheck" : true, + "minified" : true }, - "ios": { - "appstore": "unpackage/res/icons/1024x1024.png", - "ipad": { - "app": "unpackage/res/icons/76x76.png", - "app@2x": "unpackage/res/icons/152x152.png", - "notification": "unpackage/res/icons/20x20.png", - "notification@2x": "unpackage/res/icons/40x40.png", - "proapp@2x": "unpackage/res/icons/167x167.png", - "settings": "unpackage/res/icons/29x29.png", - "settings@2x": "unpackage/res/icons/58x58.png", - "spotlight": "unpackage/res/icons/40x40.png", - "spotlight@2x": "unpackage/res/icons/80x80.png" - }, - "iphone": { - "app@2x": "unpackage/res/icons/120x120.png", - "app@3x": "unpackage/res/icons/180x180.png", - "notification@2x": "unpackage/res/icons/40x40.png", - "notification@3x": "unpackage/res/icons/60x60.png", - "settings@2x": "unpackage/res/icons/58x58.png", - "settings@3x": "unpackage/res/icons/87x87.png", - "spotlight@2x": "unpackage/res/icons/80x80.png", - "spotlight@3x": "unpackage/res/icons/120x120.png" - } + "permission" : { + "scope.bluetooth" : { + "desc" : "蓝牙将用于控制和管理您的智能门锁" + } + }, + "usingComponents" : true, + "lazyCodeLoading" : "requiredComponents", + "optimization" : { + "subPackages" : true + }, + "plugins" : { + "wmpf-voip" : { + "version" : "latest", + "provider" : "wxf830863afde621eb", + "genericsImplementation" : { + "call-page-plugin" : { + "custombox" : "pages/main/customBox" + } + } + } } - }, - "android": { - "permissions": [ - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "" - ], - "targetSdkVersion": 34, - "abiFilters": ["armeabi-v7a", "arm64-v8a"] - }, - "ios": { - "dSYMs": false - } }, - "modules": { - "Bluetooth": {}, - "VideoPlayer": {}, - "Camera": {}, - "Record": {} - }, - "splashscreen": { - "waiting": false + "vueVersion" : "3", + "app-plus" : { + "distribute" : { + "icons" : { + "android" : { + "hdpi" : "unpackage/res/icons/72x72.png", + "xhdpi" : "unpackage/res/icons/96x96.png", + "xxhdpi" : "unpackage/res/icons/144x144.png", + "xxxhdpi" : "unpackage/res/icons/192x192.png" + }, + "ios" : { + "appstore" : "unpackage/res/icons/1024x1024.png", + "ipad" : { + "app" : "unpackage/res/icons/76x76.png", + "app@2x" : "unpackage/res/icons/152x152.png", + "notification" : "unpackage/res/icons/20x20.png", + "notification@2x" : "unpackage/res/icons/40x40.png", + "proapp@2x" : "unpackage/res/icons/167x167.png", + "settings" : "unpackage/res/icons/29x29.png", + "settings@2x" : "unpackage/res/icons/58x58.png", + "spotlight" : "unpackage/res/icons/40x40.png", + "spotlight@2x" : "unpackage/res/icons/80x80.png" + }, + "iphone" : { + "app@2x" : "unpackage/res/icons/120x120.png", + "app@3x" : "unpackage/res/icons/180x180.png", + "notification@2x" : "unpackage/res/icons/40x40.png", + "notification@3x" : "unpackage/res/icons/60x60.png", + "settings@2x" : "unpackage/res/icons/58x58.png", + "settings@3x" : "unpackage/res/icons/87x87.png", + "spotlight@2x" : "unpackage/res/icons/80x80.png", + "spotlight@3x" : "unpackage/res/icons/120x120.png" + } + } + }, + "android" : { + "permissions" : [ + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "" + ], + "targetSdkVersion" : 34, + "abiFilters" : [ "armeabi-v7a", "arm64-v8a" ] + }, + "ios" : { + "dSYMs" : false + } + }, + "modules" : { + "Bluetooth" : {}, + "VideoPlayer" : {}, + "Camera" : {}, + "Record" : {} + }, + "splashscreen" : { + "waiting" : false + } } - } } diff --git a/pages/main/lockDetail.vue b/pages/main/lockDetail.vue index 05cd56a..3cc70b9 100644 --- a/pages/main/lockDetail.vue +++ b/pages/main/lockDetail.vue @@ -314,7 +314,7 @@ }) const jumpToPlayer = async () => { - // #ifdef APP-ANDROID + // #ifdef APP-PLUS // 检查权限 const result = await requestPermission() if (result.code !== 0) { diff --git a/pages/p2p/p2pPlayer.vue b/pages/p2p/p2pPlayer.vue index c03baa5..7f287bf 100644 --- a/pages/p2p/p2pPlayer.vue +++ b/pages/p2p/p2pPlayer.vue @@ -193,7 +193,7 @@ stopServiceFunction, runSendServiceFunction, stopSendServiceFunction, - dataSend + dataSendFunction } from '@/uni_modules/xhj-tencent-xp2p' import { initAudio, onStartRecord, stopRecord, releaseRecord } from '@/uni_modules/xhj-record' // #endif @@ -224,6 +224,7 @@ const isMute = ref(false) const isVideoLoaded = ref(false) const videoKey = ref(Date.now()) + const cleanupCalled = ref(false) const advancedOptions = ref([ { @@ -249,6 +250,34 @@ return data }) + const cleanupResources = () => { + if (cleanupCalled.value) return + cleanupCalled.value = true + + // #ifdef APP-PLUS + // 停止录音 + url.value = null + isVideoLoaded.value = false + if (isVoice.value) { + stopRecord() + } + releaseRecord() + if (deviceInfo.value) { + stopSendServiceFunction(`${deviceInfo.value.productId}/${deviceInfo.value.deviceName}`) + stopServiceFunction(`${deviceInfo.value.productId}/${deviceInfo.value.deviceName}`) + } + // #endif + // #ifdef MP-WEIXIN + const page = getCurrentPages().pop() + if (page) { + const player = page.selectComponent('#playerRef') + if (player && player.stopAll) { + player.stopAll() + } + } + // #endif + } + onMounted(async () => { // #ifdef APP-PLUS initAudio() @@ -307,7 +336,7 @@ runSendServiceFunction( `${deviceInfo.value.productId}/${deviceInfo.value.deviceName}`, 'channel=0', - true + false ) }, 0) } else { @@ -320,19 +349,7 @@ }) onUnload(() => { - // #ifdef APP-PLUS - // 停止录音 - releaseRecord() - stopSendServiceFunction(`${deviceInfo.value.productId}/${deviceInfo.value.deviceName}`) - stopServiceFunction(`${deviceInfo.value.productId}/${deviceInfo.value.deviceName}`) - // #endif - // #ifdef MP-WEIXIN - const page = getCurrentPages().pop() - const player = page.selectComponent('#playerRef') - if (player.stopAll) { - player.stopAll() - } - // #endif + cleanupResources() }) const showQualitySelector = () => { @@ -384,13 +401,7 @@ } const handleHangUp = () => { - // #ifdef MP-WEIXIN - const page = getCurrentPages().pop() - const player = page.selectComponent('#playerRef') - if (player.stopAll) { - player.stopAll() - } - // #endif + cleanupResources() uni.navigateBack() } @@ -535,7 +546,7 @@ } const callback = audioData => { - dataSend(`${deviceInfo.value.productId}/${deviceInfo.value.deviceName}`, audioData) + dataSendFunction(`${deviceInfo.value.productId}/${deviceInfo.value.deviceName}`, audioData) } const handlePlaySuccess = () => { diff --git a/uni_modules/xhj-record/utssdk/app-ios/Info.plist b/uni_modules/xhj-record/utssdk/app-ios/Info.plist new file mode 100644 index 0000000..50fed37 --- /dev/null +++ b/uni_modules/xhj-record/utssdk/app-ios/Info.plist @@ -0,0 +1,8 @@ + + + + + NSMicrophoneUsageDescription + 需要访问麦克风进行录音 + + diff --git a/uni_modules/xhj-record/utssdk/app-ios/hybrid.swift b/uni_modules/xhj-record/utssdk/app-ios/hybrid.swift new file mode 100644 index 0000000..c5194f2 --- /dev/null +++ b/uni_modules/xhj-record/utssdk/app-ios/hybrid.swift @@ -0,0 +1,244 @@ +import Foundation +import AVFoundation + +@objc +public class RecordPermission: NSObject { + @objc + public static func requestRecordPermission(_ completion: @escaping (Bool) -> Void) { + AVAudioSession.sharedInstance().requestRecordPermission { granted in + completion(granted) + } + } +} + +@objc +public class AudioRecorderManager: NSObject { + private var audioEngine: AVAudioEngine? + private var audioConverter: AVAudioConverter? + private var aacBuffer: AVAudioCompressedBuffer? + + private let lock = NSLock() + private var _isRecording = false + var isRecording: Bool { + get { + lock.lock() + defer { lock.unlock() } + return _isRecording + } + set { + lock.lock() + _isRecording = newValue + lock.unlock() + } + } + + @objc + public static let shared = AudioRecorderManager() + + private override init() {} + + @objc + public func initAudio(_ completion: @escaping (Bool, String) -> Void) { + completion(true, "Module initialized") + } + + @objc + public func startRecord(_ completion: @escaping (Data?, Bool, String) -> Void) { + if self.isRecording { + completion(nil, false, "Recording is already in progress.") + return + } + + let session = AVAudioSession.sharedInstance() + do { + try session.setCategory(.playAndRecord, mode: .default, options: .defaultToSpeaker) + try session.setPreferredSampleRate(16000.0) + try session.setPreferredInputNumberOfChannels(1) + try session.setActive(true) + } catch { + completion(nil, false, "Failed to set up audio session: \(error.localizedDescription)") + return + } + + audioEngine = AVAudioEngine() + guard let audioEngine = audioEngine else { + completion(nil, false, "Failed to create audio engine") + return + } + + let inputNode = audioEngine.inputNode + let inputFormat = inputNode.outputFormat(forBus: 0) + + var outputFormatDescription = AudioStreamBasicDescription( + mSampleRate: 16000.0, + mFormatID: kAudioFormatMPEG4AAC, + mFormatFlags: 2, + mBytesPerPacket: 0, + mFramesPerPacket: 1024, + mBytesPerFrame: 0, + mChannelsPerFrame: 1, + mBitsPerChannel: 0, + mReserved: 0 + ) + + guard let outputFormat = AVAudioFormat(streamDescription: &outputFormatDescription) else { + completion(nil, false, "Failed to create output audio format") + return + } + + guard let converter = AVAudioConverter(from: inputFormat, to: outputFormat) else { + completion(nil, false, "Failed to create audio converter") + return + } + self.audioConverter = converter + + self.aacBuffer = AVAudioCompressedBuffer( + format: outputFormat, + packetCapacity: 1, + maximumPacketSize: converter.maximumOutputPacketSize + ) + + inputNode.installTap(onBus: 0, bufferSize: 1024, format: inputFormat) { [weak self] (pcmBuffer, when) in + guard let self = self, self.isRecording else { return } + self.convert(pcmBuffer: pcmBuffer, completion: completion) + } + + do { + audioEngine.prepare() + try audioEngine.start() + self.isRecording = true + completion(nil, true, "Recording started") + } catch { + self.isRecording = false + completion(nil, false, "Failed to start audio engine: \(error.localizedDescription)") + } + } + + private func convert(pcmBuffer: AVAudioPCMBuffer, completion: @escaping (Data?, Bool, String) -> Void) { + guard let converter = self.audioConverter, let outputBuffer = self.aacBuffer else { return } + + outputBuffer.byteLength = 0 + outputBuffer.packetCount = 0 + + var error: NSError? + var pcmBufferWasProvided = false + let status = converter.convert(to: outputBuffer, error: &error) { _, outStatus in + if pcmBufferWasProvided { + outStatus.pointee = .noDataNow + return nil + } + outStatus.pointee = .haveData + pcmBufferWasProvided = true + return pcmBuffer + } + + guard status != .error, error == nil else { + print("AAC conversion error: \(error?.localizedDescription ?? "unknown")") + return + } + + if outputBuffer.byteLength == 0 { + return + } + + let aacData = Data(bytes: outputBuffer.data, count: Int(outputBuffer.byteLength)) + + guard let adtsHeader = self.adtsHeader(for: aacData.count) else { + print("Failed to create ADTS header") + return + } + + var fullPacket = Data() + fullPacket.append(Data(adtsHeader)) + fullPacket.append(aacData) + + completion(fullPacket, true, "") + } + + private func adtsHeader(for aacFrameSize: Int) -> [UInt8]? { + guard let outputFormat = self.audioConverter?.outputFormat else { return nil } + + let adtsLength = aacFrameSize + 7 + let sampleRate = outputFormat.sampleRate + let channels = outputFormat.channelCount + + let sampleRateIndices: [Double: Int] = [ + 96000: 0, 88200: 1, 64000: 2, 48000: 3, 44100: 4, 32000: 5, + 24000: 6, 22050: 7, 16000: 8, 12000: 9, 11025: 10, 8000: 11, 7350: 12 + ] + guard let freqIndex = sampleRateIndices[sampleRate] else { + print("Unsupported sample rate for ADTS header: \(sampleRate)") + return nil + } + + let profile = 2 // AAC-LC + let channelCfg = channels + + var adtsHeader = [UInt8](repeating: 0, count: 7) + adtsHeader[0] = 0xFF + adtsHeader[1] = 0xF9 + adtsHeader[2] = UInt8(((profile - 1) << 6) | (freqIndex << 2) | (Int(channelCfg) >> 2)) + adtsHeader[3] = UInt8((Int(channelCfg) & 3) << 6 | (adtsLength >> 11)) + adtsHeader[4] = UInt8((adtsLength & 0x7FF) >> 3) + adtsHeader[5] = UInt8(((adtsLength & 7) << 5) | 0x1F) + adtsHeader[6] = 0xFC + + return adtsHeader + } + + @objc + public func stopRecord(_ completion: @escaping (Bool, String, String) -> Void) { + guard self.isRecording else { + completion(false, "Recording is not in progress.", "") + return + } + self.isRecording = false + + audioEngine?.stop() + audioEngine?.inputNode.removeTap(onBus: 0) + audioEngine = nil + audioConverter = nil + aacBuffer = nil + + do { + try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation) + } catch { + print("Failed to deactivate audio session: \(error)") + } + + completion(true, "Recording stopped", "") + } + + @objc + public func releaseRecord(_ completion: @escaping (Bool, String) -> Void) { + if self.isRecording { + self.isRecording = false + audioEngine?.stop() + audioEngine?.inputNode.removeTap(onBus: 0) + } + audioEngine = nil + audioConverter = nil + aacBuffer = nil + + do { + try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation) + } catch { + print("Failed to deactivate audio session on release: \(error)") + } + + completion(true, "Record released") + } +} + +@objc +public class UTSConversionHelper: NSObject { + @objc + public static func dataToNSArray(_ data: Data) -> NSArray { + let byteArray = [UInt8](data) + let nsArray = NSMutableArray() + for byte in byteArray { + nsArray.add(NSNumber(value: byte)) + } + return nsArray + } +} diff --git a/uni_modules/xhj-record/utssdk/app-ios/index.uts b/uni_modules/xhj-record/utssdk/app-ios/index.uts index e69de29..f8e9a74 100644 --- a/uni_modules/xhj-record/utssdk/app-ios/index.uts +++ b/uni_modules/xhj-record/utssdk/app-ios/index.uts @@ -0,0 +1,85 @@ +/* eslint-disable */ +// @ts-nocheck +// @ts-ignore-start + +import { Result } from '../interface.uts' + +function dataToByteArray(data: Data): Array { + const nsArray = UTSConversionHelper.dataToNSArray(data) + const buffer: Array = [] + if (nsArray != null) { + for (const item of nsArray) { + const num = item as NSNumber + buffer.push(num.uint8Value) + } + } + return buffer +} + +export const requestPermission = async function (): Promise { + return new Promise(resolve => { + RecordPermission.requestRecordPermission((granted: boolean) => { + if (granted) { + resolve({ code: 0, data: {}, message: '成功' }) + } else { + resolve({ code: -1, data: {}, message: '失败' }) + } + }) + }) +} + +export const initAudio = async function (): Promise { + return new Promise(resolve => { + AudioRecorderManager.shared.initAudio((success: boolean, message: string) => { + if (success) { + resolve({ code: 0, data: {}, message: message }) + } else { + resolve({ code: -1, data: {}, message: message }) + } + }) + }) +} + +export async function onStartRecord(callback: (data: Array) => void): Promise { + return new Promise(resolve => { + AudioRecorderManager.shared.startRecord( + (data: Data | null, success: boolean, message: string) => { + if (data != null) { + callback(dataToByteArray(data!)) + } else if (success) { + resolve({ code: 0, data: {}, message: message }) + } else { + resolve({ code: -1, data: {}, message: message }) + } + } + ) + }) +} + +export const stopRecord = async function (): Promise { + return new Promise(resolve => { + AudioRecorderManager.shared.stopRecord( + (success: boolean, message: string, filePath: string) => { + if (success) { + resolve({ code: 0, data: { filePath: filePath }, message: message }) + } else { + resolve({ code: -1, data: {}, message: message }) + } + } + ) + }) +} + +export const releaseRecord = async function (): Promise { + return new Promise(resolve => { + AudioRecorderManager.shared.releaseRecord((success: boolean, message: string) => { + if (success) { + resolve({ code: 0, data: {}, message: message }) + } else { + resolve({ code: -1, data: {}, message: message }) + } + }) + }) +} + +// @ts-ignore-end diff --git a/uni_modules/xhj-tencent-xp2p/utssdk/app-android/index.uts b/uni_modules/xhj-tencent-xp2p/utssdk/app-android/index.uts index e23f1ab..e4c4919 100644 --- a/uni_modules/xhj-tencent-xp2p/utssdk/app-android/index.uts +++ b/uni_modules/xhj-tencent-xp2p/utssdk/app-android/index.uts @@ -158,7 +158,7 @@ export const stopSendServiceFunction = async function (id: string): Promise): Promise { +export const dataSendFunction = async function (id: string, data: Array): Promise { try { let byteArray = new ByteArray(data.length.toInt()) diff --git a/uni_modules/xhj-tencent-xp2p/utssdk/app-ios/hybrid.swift b/uni_modules/xhj-tencent-xp2p/utssdk/app-ios/hybrid.swift index 6e64df8..27e5f4a 100644 --- a/uni_modules/xhj-tencent-xp2p/utssdk/app-ios/hybrid.swift +++ b/uni_modules/xhj-tencent-xp2p/utssdk/app-ios/hybrid.swift @@ -7,7 +7,30 @@ public class P2PConversionHelper: NSObject { guard let pointer = cString else { return nil } - // Use explicit UTF-8 encoding for safety, especially with URLs. return String(cString: pointer, encoding: .utf8) } } + +@objc +public class XP2PDataHelper: NSObject { + @objc + public static func createBytes(_ array: NSArray) -> UnsafeMutablePointer? { + let count = array.count + guard count > 0 else { return nil } + + let pointer = UnsafeMutablePointer.allocate(capacity: count) + for i in 0..?) { + pointer?.deallocate() + } +} diff --git a/uni_modules/xhj-tencent-xp2p/utssdk/app-ios/index.uts b/uni_modules/xhj-tencent-xp2p/utssdk/app-ios/index.uts index 560702a..e4cde93 100644 --- a/uni_modules/xhj-tencent-xp2p/utssdk/app-ios/index.uts +++ b/uni_modules/xhj-tencent-xp2p/utssdk/app-ios/index.uts @@ -74,7 +74,7 @@ export const startServiceFunction = async function ( config.cross = true // 设置回调 - setUserCallbackToXp2p(avRecvHandle, msgHandle, deviceDataHandle) + setUserCallbackToXp2p(null, msgHandle, null) const result = startService(deviceId, productId, deviceName, config) @@ -132,8 +132,8 @@ export const runSendServiceFunction = async function ( crypto: boolean ): Promise { try { - // const result = await runSendService(id, cmd, crypto) - // console.log('开始发送服务', result) + const result = runSendService(id, cmd, crypto) + console.log('开始发送服务', result) return { code: 0, data: {}, @@ -150,9 +150,39 @@ export const runSendServiceFunction = async function ( export const stopSendServiceFunction = async function (id: string): Promise { try { - // const nullPointer = P2PPointerHelper.createNullPointer() - // const result = await stopSendService(id, nullPointer) - // console.log('停止发送服务', result) + const result = stopSendService(id, null) + console.log('停止发送服务', result) + return { + code: 0, + data: {}, + message: '成功' + } + } catch (error) { + return { + code: -1, + data: {}, + message: error.toString() + } + } +} + +export const dataSendFunction = async function (id: string, data: Array): Promise { + const buffer = XP2PDataHelper.createBytes(data as NSArray) + + if (buffer == null) { + return { + code: -1, + data: {}, + message: '创建数据缓冲区失败' + } + } + + try { + const result = dataSend(id, buffer, data.length) + // console.log('发送数据结果', result) + + XP2PDataHelper.deallocateBytes(buffer) + return { code: 0, data: {},