Merge branch 'fanpeng' into 'develop'

Fanpeng

See merge request StarlockTeam/wx-starlock!46
This commit is contained in:
范鹏 2025-07-04 10:55:30 +00:00
commit 09ddaea597
32 changed files with 1441 additions and 240 deletions

View File

@ -81,7 +81,7 @@
listenerUI: {
cameraRotation: 270,
objectFit: 'fill',
enableToggleCamera: false
enableToggleCamera: true
}
})
wmpfVoip.setVoipEndPagePath({

View File

@ -104,7 +104,6 @@
"modules" : {
"Bluetooth" : {},
"VideoPlayer" : {},
"LivePusher" : {},
"Camera" : {},
"Record" : {}
},

View File

@ -288,6 +288,10 @@
import { deleteKeyRequest } from '@/api/key'
import { transportType } from '@/constant/transportType'
// #ifdef APP-PLUS
import { requestPermission } from '@/uni_modules/xhj-record'
// #endif
const $bluetooth = useBluetoothStore()
const $basic = useBasicStore()
const $lock = useLockStore()
@ -309,7 +313,18 @@
await getServeTime()
})
const jumpToPlayer = () => {
const jumpToPlayer = async () => {
// #ifdef APP-PLUS
//
const result = await requestPermission()
if (result.code !== 0) {
uni.showToast({
title: '请在设置中打开应用的麦克风权限',
icon: 'none'
})
return
}
// #endif
$basic.routeJump({ name: 'p2pPlayer' })
}

View File

@ -25,15 +25,16 @@
<!-- #ifdef APP-PLUS -->
<video
:key="videoKey"
autoplay
id="playerRef"
v-if="url"
:muted="isMute"
:src="urlPrefix"
:advanced="advancedOptions"
object-fit="cover"
class="w-[100vw] h-[100vh]"
:is-live="true"
:play-strategy="2"
:controls="false"
:show-progress="false"
:show-fullscreen-btn="false"
@ -50,7 +51,7 @@
></image>
<cover-view
v-if="isApp || buttonInfo"
v-if="(isApp || buttonInfo) && isVideoLoaded"
:style="{
top: isApp ? '60px' : buttonInfo.bottom + 15 + 'px'
}"
@ -188,13 +189,13 @@
// #ifdef APP-PLUS
import {
startService,
getLiveUrl,
stopService,
runSendService,
stopSendService,
dataSend
startServiceFunction,
stopServiceFunction,
runSendServiceFunction,
stopSendServiceFunction,
dataSendFunction
} from '@/uni_modules/xhj-tencent-xp2p'
import { initAudio, onStartRecord, stopRecord, releaseRecord } from '@/uni_modules/xhj-record'
// #endif
const $bluetooth = useBluetoothStore()
@ -222,16 +223,65 @@
const isVoice = ref(false)
const isMute = ref(false)
const isVideoLoaded = ref(false)
const videoKey = ref(Date.now())
const cleanupCalled = ref(false)
const advancedOptions = ref([
{
key: 'videotoolbox',
value: 0,
type: 'player'
},
{
key: 'framedrop',
value: 5,
type: 'player'
},
{
key: 'skip_loop_filter',
value: 48,
type: 'player'
}
])
const urlPrefix = computed(() => {
return url.value + `ipc.flv?action=live&channel=0&quality=${range.value[index.value].value}`
const data =
url.value + `ipc.flv?action=live&channel=0&quality=${range.value[index.value].value}`
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()
isApp.value = true
//
initRecorder()
// #endif
// #ifdef MP-WEIXIN
@ -263,30 +313,32 @@
// #ifdef APP-PLUS
if (code === 0) {
deviceInfo.value = data
const result = await startService({
const params = {
appKey: 'aanuJXFtISXFYVVsd',
appSecret: 'SsnOMHJUcazCvpULSVWY',
productId: deviceInfo.value.productId,
deviceName: deviceInfo.value.deviceName,
xp2pInfo: deviceInfo.value.xp2pInfo
})
if (result.code === 0) {
setTimeout(async () => {
const urlResult = await getLiveUrl({
id: `${deviceInfo.value.productId}/${deviceInfo.value.deviceName}`
})
if (urlResult.code === 0) {
url.value = urlResult.data.url
runSendService(
`${deviceInfo.value.productId}/${deviceInfo.value.deviceName}`,
'channel=0',
true
)
handlePlaySuccess()
} else {
$basic.backAndToast(message)
}
}, 200)
}
const result = await startServiceFunction(
params.appKey,
params.appSecret,
params.productId,
params.deviceName,
params.xp2pInfo
)
console.log('初始化SDK结果', result)
if (result?.code === 0) {
handlePlaySuccess()
setTimeout(() => {
url.value = result.data.url
runSendServiceFunction(
`${deviceInfo.value.productId}/${deviceInfo.value.deviceName}`,
'channel=0',
false
)
}, 0)
} else {
$basic.backAndToast(message)
}
@ -297,31 +349,19 @@
})
onUnload(() => {
// #ifdef APP-PLUS
//
if (isVoice.value && recorderManager.value) {
stopRecording()
}
stopSendService(`${deviceInfo.value.productId}/${deviceInfo.value.deviceName}`)
stopService({
id: `${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 = () => {
uni.showActionSheet({
itemList: range.value.map(item => item.name),
success: res => {
index.value = res.tapIndex
const value = res.tapIndex
if (index.value === value) {
return
}
index.value = value
videoKey.value = Date.now()
}
})
}
@ -361,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()
}
@ -480,104 +514,6 @@
return false
}
// #ifdef APP-PLUS
const recorderManager = ref(null)
const recordingFilePath = ref('')
// #endif
const convertAudioFileToUint8Array = filePath => {
// #ifdef APP-PLUS
try {
plus.io.resolveLocalFileSystemURL(filePath, entry => {
entry.file(
file => {
console.log(11111, file)
if (file.size === 0) return
const fileReader = new plus.io.FileReader()
fileReader.onloadend = async evt => {
try {
console.log(evt)
const base64 = evt.target.result.split(',')[1]
const binaryString = atob(base64)
const byteArray = []
for (let i = 0; i < binaryString.length; i++) {
byteArray[i] = binaryString.charCodeAt(i)
}
const id = `${deviceInfo.value.productId}/${deviceInfo.value.deviceName}`
await dataSend(id, byteArray)
} catch (error) {
console.error('音频数据转换失败:', error)
}
}
fileReader.onerror = () => {
console.error('音频文件读取失败')
}
fileReader.readAsDataURL(file)
},
error => console.error('获取文件对象失败:', error)
)
})
} catch (error) {
console.error('音频文件处理异常:', error)
}
// #endif
}
const initRecorder = () => {
// #ifdef APP-PLUS
recorderManager.value = uni.getRecorderManager()
recorderManager.value.onStart(() => {
console.log('录音开始')
})
recorderManager.value.onError(error => {
console.error('录音出错:', error)
})
recorderManager.value.onStop(res => {
console.log('录音结束:', res)
recordingFilePath.value = res.tempFilePath
convertAudioFileToUint8Array(res.tempFilePath)
})
// #endif
}
//
const startRecording = () => {
// #ifdef APP-PLUS
if (!recorderManager.value) {
initRecorder()
}
recorderManager.value.start({
duration: 60000,
sampleRate: 16000,
format: 'aac'
})
console.log('开始录音')
// #endif
}
//
const stopRecording = () => {
// #ifdef APP-PLUS
if (recorderManager.value) {
recorderManager.value.stop()
console.log('停止录音')
}
// #endif
}
const toggleVoice = () => {
// #ifdef MP-WEIXIN
if (isVoice.value) {
@ -600,17 +536,19 @@
// #ifdef APP-PLUS
if (isVoice.value) {
isVoice.value = false
stopRecording()
stopRecord()
} else {
uni.vibrateLong()
isVoice.value = true
//
startRecording()
onStartRecord(callback)
}
// #endif
}
const callback = audioData => {
dataSendFunction(`${deviceInfo.value.productId}/${deviceInfo.value.deviceName}`, audioData)
}
const handlePlaySuccess = () => {
isVideoLoaded.value = true
}

View File

@ -0,0 +1,83 @@
{
"id": "xhj-record",
"displayName": "xhj-record",
"version": "1.0.0",
"description": "xhj-record",
"keywords": [
"xhj-record"
],
"repository": "",
"engines": {
"HBuilderX": "^3.6.8"
},
"dcloudext": {
"type": "uts",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "",
"data": "",
"permissions": ""
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "u",
"aliyun": "u",
"alipay": "u"
},
"client": {
"Vue": {
"vue2": "u",
"vue3": "u"
},
"App": {
"app-android": "u",
"app-ios": "u",
"app-harmony": "u"
},
"H5-mobile": {
"Safari": "u",
"Android Browser": "u",
"微信浏览器(Android)": "u",
"QQ浏览器(Android)": "u"
},
"H5-pc": {
"Chrome": "u",
"IE": "u",
"Edge": "u",
"Firefox": "u",
"Safari": "u"
},
"小程序": {
"微信": "u",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
package="io.dcloud.nativeresouce">
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<application>
<!--meta-data-->
</application>
</manifest>

View File

@ -0,0 +1,3 @@
{
"minSdkVersion": "21"
}

View File

@ -0,0 +1,209 @@
/* eslint-disable */
// @ts-nocheck
// @ts-ignore-start
import 'android.media.AudioRecord'
import 'android.media.MediaRecorder'
import 'android.media.AudioFormat'
import 'android.media.MediaSyncEvent'
import 'java.lang.Thread'
import { UTSAndroid } from 'io.dcloud.uts'
import { Result } from '../interface.uts'
let recorder: AudioRecord | null = null
let recorderState: boolean = false
let bufferSizeInBytes: Int = 0
export const requestPermission = async function (): Promise<Result> {
return new Promise(resolve => {
try {
let permissionNeed = ['android.permission.RECORD_AUDIO']
UTSAndroid.requestSystemPermission(
UTSAndroid.getUniActivity()!,
permissionNeed,
function (allRight: boolean, _: string[]) {
if (allRight) {
resolve({
code: 0,
data: {},
message: '成功'
})
} else {
resolve({
code: -1,
data: {},
message: '失败'
})
}
},
function (_: boolean, _: string[]) {
resolve({
code: -1,
data: {},
message: '失败'
})
}
)
} catch (error) {
resolve({
code: -1,
data: {},
message: error.toString()
})
}
})
}
export const initAudio = async function (): Promise<Result> {
try {
const audioSource = MediaRecorder.AudioSource.MIC
const sampleRateInHz = 44100
const channelConfig = AudioFormat.CHANNEL_IN_MONO
const audioFormat = AudioFormat.ENCODING_PCM_16BIT
bufferSizeInBytes = AudioRecord.getMinBufferSize(
sampleRateInHz.toInt(),
channelConfig,
audioFormat
)
console.log('bufferSizeInBytes', bufferSizeInBytes)
recorder = new AudioRecord(
audioSource,
sampleRateInHz.toInt(),
channelConfig,
audioFormat,
bufferSizeInBytes
)
const currentRecorder = recorder
if (currentRecorder !== null) {
console.log('初始化录音结果:', currentRecorder.getState())
if (currentRecorder.getState() == AudioRecord.STATE_INITIALIZED) {
return {
code: 0,
data: {},
message: '成功'
}
}
}
return {
code: -1,
data: {},
message: '初始化录音失败'
}
} catch (error) {
console.log('初始化录音失败', error)
return {
code: -1,
data: {},
message: error.toString()
}
}
}
export async function onStartRecord(callback: (data: Array) => void): Promise<Result> {
try {
const callbackFunction = () => {
const currentRecorder = recorder
if (currentRecorder !== null) {
while (recorderState) {
let audioData = new ByteArray(bufferSizeInBytes)
const result: Int = currentRecorder.read(audioData, 0, bufferSizeInBytes)
if (result > 0) {
callback(Array.fromNative(audioData))
console.log('录音按帧返回数据', Array.fromNative(audioData))
}
Thread.sleep(10)
}
}
return
}
const currentRecorder = recorder
if (currentRecorder !== null) {
currentRecorder.startRecording()
console.log('开始录音')
recorderState = true
callbackFunction()
return {
code: 0,
data: {},
message: '成功'
}
}
return {
code: -1,
data: {},
message: '开始录音失败'
}
} catch (error) {
console.log('开始录音失败', error)
return {
code: -1,
data: {},
message: error.toString()
}
}
}
export const stopRecord = async function (): Promise<Result> {
try {
const currentRecorder = recorder
if (currentRecorder !== null) {
currentRecorder.stop()
recorderState = false
console.log('停止录音')
return {
code: 0,
data: {},
message: '成功'
}
}
return {
code: -1,
data: {},
message: '停止录音失败'
}
} catch (error) {
console.log('停止录音失败', error)
recorderState = false
return {
code: -1,
data: {},
message: error.toString()
}
}
}
export const releaseRecord = async function (): Promise<Result> {
try {
const currentRecorder = recorder
if (currentRecorder !== null) {
currentRecorder.release()
console.log('释放录音')
return {
code: 0,
data: {},
message: '成功'
}
}
return {
code: -1,
data: {},
message: '释放录音失败'
}
} catch (error) {
console.log('释放录音失败', error)
return {
code: -1,
data: {},
message: error.toString()
}
}
}
// @ts-ignore-end

View File

@ -0,0 +1,8 @@
<?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>NSMicrophoneUsageDescription</key>
<string>需要访问麦克风进行录音</string>
</dict>
</plist>

View File

@ -0,0 +1,3 @@
{
"deploymentTarget": "12"
}

View File

@ -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
}
}

View File

@ -0,0 +1,85 @@
/* eslint-disable */
// @ts-nocheck
// @ts-ignore-start
import { Result } from '../interface.uts'
function dataToByteArray(data: Data): Array<UInt8> {
const nsArray = UTSConversionHelper.dataToNSArray(data)
const buffer: Array<UInt8> = []
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<Result> {
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<Result> {
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<UInt8>) => void): Promise<Result> {
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<Result> {
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<Result> {
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

View File

@ -0,0 +1,5 @@
export type Result = {
code: number
data: object
message: string
}

View File

@ -1,49 +1,97 @@
/* eslint-disable */
// @ts-nocheck
// @ts-ignore-start
import { UTSAndroid } from 'io.dcloud.uts'
import { XP2PAppConfig, XP2P } from 'com.tencent.xnet'
import { Result, InitParams, IdParams } from '../interface.uts'
import { XP2PAppConfig, XP2P, XP2PCallback } from 'com.tencent.xnet'
import { Result } from '../interface.uts'
export const startService = async function (params: InitParams): Promise<Result> {
try {
const context = UTSAndroid.getAppContext()
class XP2PCallbackImpl extends XP2PCallback {
private eventNotifyCallback?: (id: string | null, msg: string | null, event: Int) => void
let xp2pConfig: XP2PAppConfig = new XP2PAppConfig()
constructor(eventNotifyCallback?: (id: string | null, msg: string | null, event: Int) => void) {
super()
this.eventNotifyCallback = eventNotifyCallback
}
xp2pConfig.appKey = params.appKey
xp2pConfig.appSecret = params.appSecret
override fail(msg: string | null, errorCode: Int): void {
console.log('XP2P callback fail:', msg, errorCode)
}
await XP2P.startService(
context,
params.productId,
params.deviceName,
params.xp2pInfo,
xp2pConfig
)
override commandRequest(id: string | null, msg: string | null): void {
console.log('XP2P callback commandRequest:', id, msg)
}
return {
code: 0,
data: {},
message: '成功'
}
} catch (error) {
return {
code: -1,
data: {},
message: error.toString()
}
override xp2pEventNotify(id: string | null, msg: string | null, event: Int): void {
console.log('XP2P callback xp2pEventNotify:', id, msg, event)
this.eventNotifyCallback?.invoke(id, msg, event)
}
override avDataRecvHandle(id: string | null, data: ByteArray | null, len: Int): void {
console.log('XP2P callback avDataRecvHandle:', id, len)
}
override avDataCloseHandle(id: string | null, msg: string | null, errorCode: Int): void {
console.log('XP2P callback avDataCloseHandle:', id, msg, errorCode)
}
override onDeviceMsgArrived(id: string | null, data: ByteArray | null, len: Int): string {
console.log('XP2P callback onDeviceMsgArrived:', id, len)
return ''
}
}
export const getLiveUrl = async function (params: IdParams): Promise<Result> {
async function getLiveUrlAsync(
id: string,
resolve: (result: Result) => void,
reject: (result: Result) => void
): Promise<void> {
try {
const liveUrl = await XP2P.delegateHttpFlv(params.id)
return {
const liveUrl = await XP2P.delegateHttpFlv(id)
resolve({
code: 0,
data: {
url: liveUrl
},
message: '成功'
}
})
} catch (error) {
reject({
code: -1,
data: {},
message: error.toString()
})
}
}
export const startServiceFunction = async function (
appKey: string,
appSecret: string,
productId: string,
deviceName: string,
xp2pInfo: string
): Promise<Result> {
try {
const context = UTSAndroid.getAppContext()
let xp2pConfig: XP2PAppConfig = new XP2PAppConfig()
xp2pConfig.appKey = appKey
xp2pConfig.appSecret = appSecret
await XP2P.startService(context, productId, deviceName, xp2pInfo, xp2pConfig)
return new Promise<Result>((resolve, reject) => {
const callback: XP2PCallback = new XP2PCallbackImpl(
(id: string | null, msg: string | null, event: Int): void => {
if (event == 1004) {
getLiveUrlAsync(id!, resolve, reject)
}
}
)
XP2P.setCallback(callback)
})
} catch (error) {
return {
code: -1,
@ -53,9 +101,9 @@ export const getLiveUrl = async function (params: IdParams): Promise<Result> {
}
}
export const stopService = async function (params: IdParams): Promise<Result> {
export const stopServiceFunction = async function (id: string): Promise<Result> {
try {
await XP2P.stopService(params.id)
await XP2P.stopService(id)
return {
code: 0,
data: {},
@ -70,21 +118,14 @@ export const stopService = async function (params: IdParams): Promise<Result> {
}
}
// export const setCallback = function (callback: XP2PCallback) {
// try {
// XP2P.setCallback(callback)
// } catch (error) {
// console.log(2, error)
// }
// }
export const runSendService = async function (
export const runSendServiceFunction = async function (
id: string,
cmd: string,
crypto: boolean
): Promise<Result> {
try {
await XP2P.runSendService(id, cmd, crypto)
console.log('开始发送服务')
const result = await XP2P.runSendService(id, cmd, crypto)
console.log('开始发送服务', result)
return {
code: 0,
data: {},
@ -99,10 +140,10 @@ export const runSendService = async function (
}
}
export const stopSendService = async function (id: string): Promise<Result> {
export const stopSendServiceFunction = async function (id: string): Promise<Result> {
try {
await XP2P.stopSendService(id, null)
console.log('停止发送服务')
const result = await XP2P.stopSendService(id, null)
console.log('停止发送服务', result)
return {
code: 0,
data: {},
@ -117,33 +158,17 @@ export const stopSendService = async function (id: string): Promise<Result> {
}
}
export const dataSend = async function (id: string, data: Array<number>): Promise<Result> {
export const dataSendFunction = async function (id: string, data: Array<number>): Promise<Result> {
try {
let byteTest = new ByteArray(data.length.toInt())
// byteTest.set(0, (0xff).toByte())
// byteTest.set(1, (0xf9).toByte())
// let profile = 2
// let freqIdx = 8
// let chanCfg = 1
// let packetLen = data.length + 7
// byteTest.set(2, (((profile - 1) << 6) + (freqIdx << 2) + (chanCfg >> 2)).toByte())
// byteTest.set(3, (((chanCfg & 3) << 6) + (packetLen >> 11)).toByte())
// byteTest.set(4, ((packetLen & 0x7ff) >> 3).toByte())
// byteTest.set(5, (((packetLen & 7) << 5) + 0x1f).toByte())
// byteTest.set(6, (0xfc).toByte())
let byteArray = new ByteArray(data.length.toInt())
for (let i = 0; i < data.length; i++) {
byteTest.set(i.toInt(), data[i].toByte())
byteArray.set(i.toInt(), data[i].toByte())
}
console.log(byteTest)
console.log(1, id, byteTest[0], byteTest[1], data.length.toInt())
const result = await XP2P.dataSend(id, byteArray, data.length.toInt())
const result = await XP2P.dataSend(id, byteTest, data.length.toInt())
console.log('发送数据', result)
console.log('发送数据结果', result, byteArray)
return {
code: 0,
@ -158,3 +183,5 @@ export const dataSend = async function (id: string, data: Array<number>): Promis
}
}
}
// @ts-ignore-end

View File

@ -0,0 +1,13 @@
<?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>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>NSAllowsArbitraryLoadsInWebContent</key>
<true/>
</dict>
</dict>
</plist>

View File

@ -0,0 +1,333 @@
#ifndef __APPWRAPPER_H_
#define __APPWRAPPER_H_
#ifdef __cplusplus
extern "C" {
#endif
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef WINDOWS
#ifdef IPCLIBRARY_EXPORTS
# define IPCLIBRARY_API __declspec(dllexport)
#else
# define IPCLIBRARY_API __declspec(dllimport)
#endif
#else
# define IPCLIBRARY_API
#endif
#define MAX_SIZE_OF_PARAMS 3000
static const char * VIDEOSDKVERSION = "2.4.x+git.2a469eda";
typedef enum
{
XP2P_PROTOCOL_AUTO = 0, //auto模式udp不通自动切换tcp
XP2P_PROTOCOL_UDP = 1, //udp传输
XP2P_PROTOCOL_TCP = 2, //tcp传输
} XP2PProtocolType;
typedef enum
{
XP2PTypeClose = 1000, //数据传输完成
XP2PTypeLog = 1001, //日志输出
XP2PTypeCmd = 1002, // command json
XP2PTypeDisconnect = 1003, // p2p链路断开
XP2PTypeDetectReady = 1004, // p2p链路初始化成功
XP2PTypeDetectError = 1005, // p2p链路初始化失败
XP2PTypeDeviceMsgArrived = 1006, // 设备端向App发消息
XP2PTypeCmdNOReturn = 1007, // 设备未返回app自定义信令
XP2PTypeStreamEnd = 1008, // 设备停止推流,或者由于达到设备最大连接数,拒绝推流
XP2PTypeDownloadEnd = 1009, // 设备停止推流,下载结束
XP2PTypeStreamRefush = 1010, // 设备拒绝推流请求的devicename不一致
XP2PTypeSaveFileOn = 8000, //获取保存音视频流开关状态
XP2PTypeSaveFileUrl = 8001 //获取音视频流保存路径
} XP2PType;
typedef enum
{
XP2PVoiceServiceClose = 2000, //语音对讲服务关闭
XP2PStreamServiceClose = 2001 //音视频流接收服务关闭
} XP2PCloseSubType;
typedef enum
{
XP2P_ERR_NONE = 0, //成功
XP2P_ERR_INIT_PRM = -1000, //入参为空
XP2P_ERR_GET_XP2PINFO = -1001, // SDK内部请求xp2p info失败
XP2P_ERR_PROXY_INIT = -1002, //本地p2p代理初始化失败
XP2P_ERR_UNINIT = -1003, //数据接收/发送服务未初始化
XP2P_ERR_ENCRYPT = -1004, //数据加密失败
XP2P_ERR_TIMEOUT = -1005, //请求超时
XP2P_ERR_REQUEST_FAIL = -1006, //请求错误
XP2P_ERR_VERSION = -1007, //设备版本过低,请升级设备固件
XP2P_ERR_APPLICATION = -1008, // application初始化失败
XP2P_ERR_REQUEST = -1009, // request初始化失败
XP2P_ERR_DETECT_NOTREADY = -1010, // p2p探测未完成
XP2P_ERR_P2P_ININED = -1011, //当前id对应的p2p已完成初始化
XP2P_ERR_P2P_UNININ = -1012, //当前id对应的p2p未初始化
XP2P_ERR_NEW_MEMERY = -1013, //内存申请失败
XP2P_ERR_XP2PINFO_RULE = -1014, //获取到的xp2p info格式错误
XP2P_ERR_XP2PINFO_DECRYPT = -1015, //获取到的xp2p info解码失败
XP2P_ERR_PROXY_LISTEN = -1016, //本地代理监听端口失败
XP2P_ERR_CLOUD_EMPTY = -1017, //云端返回空数据
XP2P_ERR_JSON_PARSE = -1018, // json解析失败
XP2P_ERR_SERVICE_NOTRUN = -1019, //当前id对应的服务(语音、直播等服务)没有在运行
XP2P_ERR_CLIENT_NULL = -1020 //从map中取出的client为空
} XP2PErrCode;
typedef struct data_report_t {
unsigned char* report_buf;
size_t report_size;
uint64_t live_size;
// uint64_t voice_size;
const char *data_action;
const char *status;
const char *uniqueId;
const char *appPeerName;
const char *deviceP2PInfo;
uint64_t appUpByte;
uint64_t appDownByte;
const char *appConnectIp;
int errorcode;
} data_report_t;
typedef struct app_config_t {
const char *server;
const char *ip;
uint64_t port;
XP2PProtocolType type;
bool cross;
} app_config_t;
typedef const char *(*msg_handle_t)(const char *id, XP2PType type, const char *msg);
typedef void (*av_recv_handle_t)(const char *id, uint8_t *recv_buf, size_t recv_len);
typedef char *(*device_data_recv_handle_t)(const char *id, uint8_t *recv_buf, size_t recv_len);
typedef void (*av_log_handle_t)(const char *id, data_report_t data_buf);
/**
* @brief
*
* @param recv_handle:
* @param msg_handle:
* @param device_data_handle: App发送消息的回调
* @return
*/
IPCLIBRARY_API void setUserCallbackToXp2p(av_recv_handle_t recv_handle, msg_handle_t msg_handle, device_data_recv_handle_t device_data_handle);
/**
* @brief camera设备并等待回复
*
* @param id: camera在app端的唯一标识符
* @param command:
* cmd_len提供16KB以内
* @param cmd_len: command长度
* @param recv_buf:
* camera回复的数据recv_len获取
* @param recv_len: camera回复的数据长度
* @param timeout_us: 0(7500ms左右)
* @return 0
*/
IPCLIBRARY_API int postCommandRequestSync(const char *id, const unsigned char *command, size_t cmd_len,
unsigned char **recv_buf, size_t *recv_len, uint64_t timeout_us);
/**
* @brief camera设备camera回复的数据由注册的回调函数返回
*
* @param id: camera在app端的唯一标识符
* @param command:
* cmd_len提供16KB以内
* @param cmd_len: command长度
* @return 0
*/
IPCLIBRARY_API int postCommandRequestWithAsync(const char *id, const unsigned char *command, size_t cmd_len);
/**
* @brief camera设备请求媒体流
*
* @param id: camera在app端的唯一标识符
* @param params: (`action=live`)(`action=playback`)
* @param crypto: (crypto=false)
* @return
*/
IPCLIBRARY_API void *startAvRecvService(const char *id, const char *params, bool crypto);
/**
* @brief
*
* @param id: camera在app端的唯一标识符
* @param req: `startAvRecvService`null
* @return 0
*/
IPCLIBRARY_API int stopAvRecvService(const char *id, void *req);
/**
* @brief xp2p服务
*
* @param id: camera在app端的唯一标识符
* @param product_id: ID
* @param device_name:
* @param sensor_timeout: tcp的超时时间
* @return 0
*/
IPCLIBRARY_API int startService(const char *id, const char *product_id, const char *device_name, app_config_t config);
/**
* @brief xp2p服务
*
* @param id: camera在app端的唯一标识符
* @param product_id: ID
* @param device_name:
* @param remote_host: ip
* @param remote_port:
* @return 0
*/
IPCLIBRARY_API int startLanService(const char *id, const char *product_id, const char *device_name,
const char *remote_host, const char *remote_port);
/**
* xp2pinfo,start时间
*/
IPCLIBRARY_API int setDeviceXp2pInfo(const char *id, const char *xp2p_info);
/**
* @brief url
*
* @param id: camera在app端的唯一标识符
* @return url
*/
IPCLIBRARY_API const char *delegateHttpFlv(const char *id);
/**
* @brief url
*
* @param id: camera在app端的唯一标识符
* @return url
*/
IPCLIBRARY_API const char *getLanUrl(const char *id);
/**
* @brief url
*
* @param id: camera在app端的唯一标识符
* @return
*/
IPCLIBRARY_API int getLanProxyPort(const char *id);
/**
* @brief camera设备发送语音或自定义数据服务
*
* @param id: camera在app端的唯一标识符
* @param params: key1=value&key2=value2
* key不允许以下划线_开头key和value中间不能包含&/+=
* @param crypto: (crypto=false)
* @return
*/
IPCLIBRARY_API void *runSendService(const char *id, const char *params, bool crypto);
/**
* @brief
*
* @param id: camera在app端的唯一标识符
* @param req: `runSendService`null
* @return
*/
IPCLIBRARY_API int stopSendService(const char *id, void *req);
/**
* @brief api信息sdk获取xp2p信息
*
* @param sec_id: API secrct_id
* @param sec_key: API secrct_key
* @return 0
*/
IPCLIBRARY_API int setQcloudApiCred(const char *sec_id, const char *sec_key);
/**
* @brief camera设备发送语音或自定义数据
*
* @param id: camera在app端的唯一标识符
* @param data:
* @param len:
* @return 0
*/
IPCLIBRARY_API int dataSend(const char *id, uint8_t *data, size_t len);
/**
* @brief xp2p服务
*
* @param id: camera在app端的唯一标识符
* @return 0
*/
IPCLIBRARY_API void stopService(const char *id);
/**
* @brief
*
* @param console:
* @param file:
*/
IPCLIBRARY_API void setLogEnable(bool console, bool file);
/*================================废弃接口=======================================*/
/**
* @brief camera设备并等待回复
*
* @param id: camera在app端的唯一标识符
* @param command:
* @param buf: camera回复的数据len获取
* @param len: camera回复的数据长度
* @param timeout_us: 0(7500ms左右)
* @return 0
*/
IPCLIBRARY_API int getCommandRequestWithSync(const char *id, const char *command, char **buf, size_t *len,
uint64_t timeout_us);
/**
* @brief camera设备camera回复的数据由注册的回调函数返回
*
* @param id:camera在app端的唯一标识符
* @param command:
* @return 0
*/
IPCLIBRARY_API int getCommandRequestWithAsync(const char *id, const char *command);
/**
* @brief 0 62 63
*
* @param id:camera在app端的唯一标识符
* @param command:
* @return 0
*/
IPCLIBRARY_API int getStreamLinkMode(const char *id);
/**
* @brief 便httpflv时
* @param id:camera在app端的唯一标识符
*/
IPCLIBRARY_API void startRecordPlayerStream(const char *id);
/**
* @brief p2p的水线超过一定值时I帧
* @param id:camera在app端的唯一标识符
*/
IPCLIBRARY_API size_t getStreamBufSize(const char *id);
IPCLIBRARY_API void setStunServerToXp2p(const char *server, uint16_t port);
IPCLIBRARY_API void setContentDetail(const char *content, const char *detail, av_log_handle_t reportdata_handle);
IPCLIBRARY_API const char* getUserID();
/**
* @brief
*/
IPCLIBRARY_API void setCrossStunTurn(bool enable);
#ifdef __cplusplus
}
#endif
#endif

View File

@ -0,0 +1,4 @@
{
"deploymentTarget": "9",
"frameworks": ["libc++.tbd", "libsqlite3.tbd", "libz.tbd"]
}

View File

@ -0,0 +1,36 @@
import Foundation
@objc
public class P2PConversionHelper: NSObject {
@objc
public static func cStringToString(_ cString: UnsafePointer<CChar>?) -> String? {
guard let pointer = cString else {
return nil
}
return String(cString: pointer, encoding: .utf8)
}
}
@objc
public class XP2PDataHelper: NSObject {
@objc
public static func createBytes(_ array: NSArray) -> UnsafeMutablePointer<UInt8>? {
let count = array.count
guard count > 0 else { return nil }
let pointer = UnsafeMutablePointer<UInt8>.allocate(capacity: count)
for i in 0..<count {
if let number = array[i] as? NSNumber {
pointer[i] = number.uint8Value
} else {
pointer[i] = 0 // Default value
}
}
return pointer
}
@objc
public static func deallocateBytes(_ pointer: UnsafeMutablePointer<UInt8>?) {
pointer?.deallocate()
}
}

View File

@ -0,0 +1,200 @@
/* eslint-disable */
// @ts-nocheck
// @ts-ignore-start
import { Result } from '../interface.uts'
const resolverMap = new Map<string, (value: Result) => void>()
function avRecvHandle(
id: Optional<UnsafePointer<Int8>>,
recv_buf: Optional<UnsafeMutablePointer<UInt8>>,
recv_len: Int
) {}
function msgHandle(
id: Optional<UnsafePointer<Int8>>,
type: XP2PType,
msg: Optional<UnsafePointer<Int8>>
): Optional<UnsafePointer<Int8>> {
if (type.rawValue == 1004) {
const idString = id != null ? P2PConversionHelper.cStringToString(id) : null
if (idString != null) {
const resolver = resolverMap.get(idString!)
if (resolver != null) {
const urlResult = delegateHttpFlv(idString!)
if (urlResult != null) {
const urlString = P2PConversionHelper.cStringToString(urlResult)
if (urlString != null && urlString != '') {
resolver!({
code: 0,
data: { url: urlString },
message: '成功'
})
resolverMap.delete(idString!)
return null
}
}
resolver!({ code: -1, data: {}, message: '获取播放URL失败' })
resolverMap.delete(idString!)
}
}
}
return null
}
function deviceDataHandle(
id: Optional<UnsafePointer<Int8>>,
recv_buf: Optional<UnsafeMutablePointer<UInt8>>,
recv_len: Int
): Optional<UnsafeMutablePointer<Int8>> {
// if (id != null) {
// const idString = P2PConversionHelper.cStringToString(id)
// // console.log(`deviceDataHandle: ${idString}, len: ${recv_len}`)
// }
return null
}
export const startServiceFunction = async function (
appKey: string,
appSecret: string,
productId: string,
deviceName: string,
xp2pInfo: string
): Promise<Result> {
const deviceId = `${productId}/${deviceName}`
return new Promise<Result>((resolve, reject) => {
resolverMap.set(deviceId, resolve)
try {
const config = app_config_t()
config.server = UnsafePointer(strdup('')!)
config.ip = UnsafePointer(strdup('')!)
config.port = 20002
config.type = XP2P_PROTOCOL_AUTO
config.cross = true
// 设置回调
setUserCallbackToXp2p(null, msgHandle, null)
const result = startService(deviceId, productId, deviceName, config)
if (NSNumber(0) === result) {
const setP2PInfoResult = setDeviceXp2pInfo(deviceId, xp2pInfo)
if (NSNumber(0) === setP2PInfoResult) {
setTimeout(() => {
if (resolverMap.has(deviceId)) {
resolve({ code: -1, data: {}, message: '获取播放URL超时' })
resolverMap.delete(deviceId)
}
}, 20000)
} else {
resolve({ code: -1, data: {}, message: 'setDeviceXp2pInfo 调用失败' })
resolverMap.delete(deviceId)
}
} else {
resolve({ code: -1, data: {}, message: 'startService 调用失败' })
resolverMap.delete(deviceId)
}
} catch (error) {
console.log('startServiceFunction 报错', error)
resolve({
code: -1,
data: {},
message: error.toString()
})
resolverMap.delete(deviceId)
}
})
}
export const stopServiceFunction = async function (id: string): Promise<Result> {
try {
const result = await stopService(id)
console.log('stopService', result)
return {
code: 0,
data: {},
message: '成功'
}
} catch (error) {
return {
code: -1,
data: {},
message: error.toString()
}
}
}
export const runSendServiceFunction = async function (
id: string,
cmd: string,
crypto: boolean
): Promise<Result> {
try {
const result = runSendService(id, cmd, crypto)
console.log('开始发送服务', result)
return {
code: 0,
data: {},
message: '成功'
}
} catch (error) {
return {
code: -1,
data: {},
message: error.toString()
}
}
}
export const stopSendServiceFunction = async function (id: string): Promise<Result> {
try {
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<number>): Promise<Result> {
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: {},
message: '成功'
}
} catch (error) {
return {
code: -1,
data: {},
message: error.toString()
}
}
}
// @ts-ignore-end

View File

@ -3,15 +3,3 @@ export type Result = {
data: object
message: string
}
export type InitParams = {
appKey: string
appSecret: string
productId: string
deviceName: string
xp2pInfo: string
}
export type IdParams = {
id: string
}