Merge branch 'fanpeng' into 'develop'

Fanpeng

See merge request StarlockTeam/wx-starlock!48
This commit is contained in:
范鹏 2025-07-11 03:29:11 +00:00
commit 2b17f261d4
25 changed files with 853 additions and 573 deletions

View File

@ -31,7 +31,7 @@
return 'XHJ' return 'XHJ'
} }
// #endif // #endif
return 'DEV' return 'PRE'
} }
}, },
computed: { computed: {

View File

@ -8,7 +8,6 @@
</view> </view>
<up-datetime-picker <up-datetime-picker
:filter="filter" :filter="filter"
:itemHeight="70"
:visibleItemCount="5" :visibleItemCount="5"
:minDate="minDate" :minDate="minDate"
:show="show" :show="show"

View File

@ -21,10 +21,10 @@ export const keysType = {
}, },
6: { 6: {
name: '掌静脉', name: '掌静脉',
icon: 'https://oss-lock.xhjcn.ltd/mp/icon_palm_vein_white.png' icon: 'https://oss-lock.xhjcn.ltd/mp/icon_palm_vein_1.png'
}, },
7: { 7: {
name: '遥控', name: '遥控',
icon: 'https://oss-lock.xhjcn.ltd/mp/icon_remote_white.png' icon: 'https://oss-lock.xhjcn.ltd/mp/icon_remote_1.png'
} }
} }

View File

@ -1,114 +1,114 @@
{ {
"name": "星星锁Lite", "name" : "星星锁Lite",
"appid": "__UNI__933D519", "appid" : "__UNI__933D519",
"description": "", "description" : "",
"versionName": "1.3.0", "versionName" : "1.3.1",
"versionCode": "37", "versionCode" : "39",
"mp-weixin": { "mp-weixin" : {
"appid": "wx9829a39e65550757", "appid" : "wx9829a39e65550757",
"setting": { "setting" : {
"urlCheck": true, "urlCheck" : true,
"minified": 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"
}, },
"ios": { "permission" : {
"appstore": "unpackage/res/icons/1024x1024.png", "scope.bluetooth" : {
"ipad": { "desc" : "蓝牙将用于控制和管理您的智能门锁"
"app": "unpackage/res/icons/76x76.png", }
"app@2x": "unpackage/res/icons/152x152.png", },
"notification": "unpackage/res/icons/20x20.png", "usingComponents" : true,
"notification@2x": "unpackage/res/icons/40x40.png", "lazyCodeLoading" : "requiredComponents",
"proapp@2x": "unpackage/res/icons/167x167.png", "optimization" : {
"settings": "unpackage/res/icons/29x29.png", "subPackages" : true
"settings@2x": "unpackage/res/icons/58x58.png", },
"spotlight": "unpackage/res/icons/40x40.png", "plugins" : {
"spotlight@2x": "unpackage/res/icons/80x80.png" "wmpf-voip" : {
}, "version" : "latest",
"iphone": { "provider" : "wxf830863afde621eb",
"app@2x": "unpackage/res/icons/120x120.png", "genericsImplementation" : {
"app@3x": "unpackage/res/icons/180x180.png", "call-page-plugin" : {
"notification@2x": "unpackage/res/icons/40x40.png", "custombox" : "pages/main/customBox"
"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": [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\" />",
"<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\" />",
"<uses-permission android:name=\"android.permission.BLUETOOTH_ADMIN\" />",
"<uses-permission android:name=\"android.permission.BLUETOOTH\" />",
"<uses-permission android:name=\"android.permission.BLUETOOTH_SCAN\" />",
"<uses-permission android:name=\"android.permission.BLUETOOTH_CONNECT\" />",
"<uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\"/>",
"<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>",
"<uses-permission android:name=\"android.permission.MANAGE_EXTERNAL_STORAGE\"/>"
],
"targetSdkVersion": 34,
"abiFilters": ["armeabi-v7a", "arm64-v8a"]
},
"ios": {
"dSYMs": false
}
}, },
"modules": { "vueVersion" : "3",
"Bluetooth": {}, "app-plus" : {
"VideoPlayer": {}, "distribute" : {
"Camera": {}, "icons" : {
"Record": {} "android" : {
}, "hdpi" : "unpackage/res/icons/72x72.png",
"splashscreen": { "xhdpi" : "unpackage/res/icons/96x96.png",
"waiting": false "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" : [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\" />",
"<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\" />",
"<uses-permission android:name=\"android.permission.BLUETOOTH_ADMIN\" />",
"<uses-permission android:name=\"android.permission.BLUETOOTH\" />",
"<uses-permission android:name=\"android.permission.BLUETOOTH_SCAN\" />",
"<uses-permission android:name=\"android.permission.BLUETOOTH_CONNECT\" />",
"<uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\"/>",
"<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>",
"<uses-permission android:name=\"android.permission.MANAGE_EXTERNAL_STORAGE\"/>"
],
"targetSdkVersion" : 34,
"abiFilters" : [ "armeabi-v7a", "arm64-v8a" ]
},
"ios" : {
"dSYMs" : false
}
},
"modules" : {
"Bluetooth" : {},
"VideoPlayer" : {},
"Camera" : {},
"Record" : {}
},
"splashscreen" : {
"waiting" : false
}
} }
}
} }

View File

@ -88,7 +88,6 @@
> >
<up-datetime-picker <up-datetime-picker
:hasInput="false" :hasInput="false"
:itemHeight="70"
:visibleItemCount="5" :visibleItemCount="5"
:show="showStartDate" :show="showStartDate"
v-model="defaultStartDate" v-model="defaultStartDate"
@ -101,7 +100,6 @@
<up-datetime-picker <up-datetime-picker
:hasInput="false" :hasInput="false"
:show="showEndDate" :show="showEndDate"
:itemHeight="70"
:visibleItemCount="5" :visibleItemCount="5"
v-model="defaultEndDate" v-model="defaultEndDate"
mode="date" mode="date"
@ -113,7 +111,6 @@
<up-datetime-picker <up-datetime-picker
:hasInput="false" :hasInput="false"
:show="showStartTime" :show="showStartTime"
:itemHeight="70"
:visibleItemCount="5" :visibleItemCount="5"
v-model="defaultStartTime" v-model="defaultStartTime"
mode="time" mode="time"
@ -125,7 +122,6 @@
<up-datetime-picker <up-datetime-picker
:hasInput="false" :hasInput="false"
:show="showEndTime" :show="showEndTime"
:itemHeight="70"
:visibleItemCount="5" :visibleItemCount="5"
v-model="defaultEndTime" v-model="defaultEndTime"
mode="time" mode="time"

View File

@ -47,7 +47,6 @@
v-model="defaultStartDate" v-model="defaultStartDate"
mode="datetime" mode="datetime"
closeOnClickOverlay closeOnClickOverlay
:itemHeight="70"
:visibleItemCount="5" :visibleItemCount="5"
@close="showStartDate = false" @close="showStartDate = false"
@confirm="confirmDate('start', $event)" @confirm="confirmDate('start', $event)"
@ -59,7 +58,6 @@
v-model="defaultEndDate" v-model="defaultEndDate"
mode="datetime" mode="datetime"
closeOnClickOverlay closeOnClickOverlay
:itemHeight="70"
:visibleItemCount="5" :visibleItemCount="5"
@close="showEndDate = false" @close="showEndDate = false"
@confirm="confirmDate('end', $event)" @confirm="confirmDate('end', $event)"

View File

@ -44,23 +44,126 @@
/> />
<!-- #endif --> <!-- #endif -->
</view> </view>
<image <view v-if="!isVideoLoaded">
v-if="!isVideoLoaded" <image
src="https://oss-lock.xhjcn.ltd/mp/background_monitor.png" src="https://oss-lock.xhjcn.ltd/mp/background_monitor.png"
class="w-full h-full absolute top-0 left-0" class="w-full h-full absolute top-0 left-0"
></image> ></image>
<view
<cover-view class="fixed bottom-[calc(48rpx+env(safe-area-inset-bottom))] w-full flex justify-center"
v-if="(isApp || buttonInfo) && isVideoLoaded" >
:style="{ <up-loading-icon
top: isApp ? '60px' : buttonInfo.bottom + 15 + 'px' size="70rpx"
}" :vertical="true"
class="bg-[rgba(0,0,0,0.35)] rounded-full px-4 py-1.5 fixed right-32" textSize="28rpx"
> text="连接中"
<cover-view class="text-white text-xs" @click="showQualitySelector"> mode="circle"
{{ range[index].name }} ></up-loading-icon>
</view>
</view>
<cover-view v-if="isVideoLoaded">
<cover-view
:style="{
top: isApp ? '60px' : buttonInfo.bottom + 15 + 'px'
}"
class="bg-[rgba(0,0,0,0.35)] rounded-full px-4 py-1.5 fixed right-32"
>
<cover-view class="text-white text-xs" @click="showQualitySelector">
{{ range[index].name }}
</cover-view>
</cover-view>
<cover-view
class="fixed bottom-[calc(32rpx+env(safe-area-inset-bottom))] left-1/2 -translate-x-1/2 w-622"
>
<cover-view
class="bg-[rgba(0,0,0,0.3)] rounded-xl p-4 shadow-lg"
style="background-color: rgba(0, 0, 0, 0.3)"
>
<cover-view class="flex items-center justify-around mx-15">
<cover-view class="relative">
<cover-image
v-show="isMute"
src="https://oss-lock.xhjcn.ltd/mp/icon_mute.png"
:class="isApp ? 'size-48' : 'size-48 p-2'"
></cover-image>
<cover-image
v-show="!isMute"
src="https://oss-lock.xhjcn.ltd/mp/icon_not_mute.png"
:class="isApp ? 'size-48' : 'size-48 p-2'"
></cover-image>
<cover-view
@click="toggleMute"
class="absolute top-0 left-0 w-full h-full size-48 p-2"
></cover-view>
</cover-view>
<cover-view class="relative">
<cover-image
src="https://oss-lock.xhjcn.ltd/mp/icon_screenshot.png"
:class="isApp ? 'size-48' : 'size-48 p-2'"
></cover-image>
<cover-view
@click="handleScreenshot"
class="absolute top-0 left-0 w-full h-full"
></cover-view>
</cover-view>
</cover-view>
<cover-view class="flex items-center justify-between text-white mt-2 px-10">
<cover-view class="flex flex-col items-center">
<cover-view
class="bg-white w-80 h-80 rounded-full flex items-center justify-center relative"
>
<cover-image
:src="
isVoice
? 'https://oss-lock.xhjcn.ltd/mp/icon_microphone.png'
: 'https://oss-lock.xhjcn.ltd/mp/icon_no_microphone.png'
"
class="w-55 h-55"
></cover-image>
<cover-view
@click="toggleVoice"
class="absolute top-0 left-0 w-full h-full"
></cover-view>
</cover-view>
<cover-view class="mt-2 text-center whitespace-nowrap text-xs">{{
isVoice ? '点击停止' : '点击说话'
}}</cover-view>
</cover-view>
<cover-view class="flex flex-col items-center">
<cover-view
class="bg-[#eb292b] w-80 h-80 rounded-full flex items-center justify-center relative"
>
<cover-image
src="https://oss-lock.xhjcn.ltd/mp/icon_hang_up.png"
class="w-60 h-60"
></cover-image>
<cover-view
@click="handleHangUp"
class="absolute top-0 left-0 w-full h-full"
></cover-view>
</cover-view>
<cover-view class="mt-2 text-center whitespace-nowrap text-xs">挂断</cover-view>
</cover-view>
<cover-view class="flex flex-col items-center">
<cover-view
class="bg-[#63b8af] w-80 h-80 rounded-full flex items-center justify-center relative"
>
<cover-image
src="https://oss-lock.xhjcn.ltd/mp/icon_lock_white.png"
class="w-60 h-60"
></cover-image>
<cover-view
@click="handleLock"
class="absolute top-0 left-0 w-full h-full"
></cover-view>
</cover-view>
<cover-view class="mt-2 text-center whitespace-nowrap text-xs">开锁</cover-view>
</cover-view>
</cover-view>
</cover-view>
</cover-view> </cover-view>
</cover-view> </cover-view>
<view class="safe-area-bottom"></view>
<!-- #ifdef MP-WEIXIN --> <!-- #ifdef MP-WEIXIN -->
<iot-p2p-voice <iot-p2p-voice
@ -72,105 +175,6 @@
> >
</iot-p2p-voice> </iot-p2p-voice>
<!-- #endif --> <!-- #endif -->
<cover-view
v-if="isVideoLoaded"
class="fixed bottom-[calc(32rpx+env(safe-area-inset-bottom))] bg-[rgba(0,0,0,0.3)] rounded-xl p-4 shadow-lg left-1/2 -translate-x-1/2 w-622"
>
<cover-view class="flex items-center justify-around mx-15">
<cover-view class="relative">
<cover-image
v-show="isMute"
src="https://oss-lock.xhjcn.ltd/mp/icon_mute.png"
:class="isApp ? 'size-48' : 'size-48 p-2'"
></cover-image>
<cover-image
v-show="!isMute"
src="https://oss-lock.xhjcn.ltd/mp/icon_not_mute.png"
:class="isApp ? 'size-48' : 'size-48 p-2'"
></cover-image>
<cover-view
@click="toggleMute"
class="absolute top-0 left-0 w-full h-full size-48 p-2"
></cover-view>
</cover-view>
<cover-view class="relative">
<cover-image
src="https://oss-lock.xhjcn.ltd/mp/icon_screenshot.png"
:class="isApp ? 'size-48' : 'size-48 p-2'"
></cover-image>
<cover-view
@click="handleScreenshot"
class="absolute top-0 left-0 w-full h-full"
></cover-view>
</cover-view>
</cover-view>
<cover-view class="flex items-center justify-between text-white mt-2 px-10">
<cover-view class="flex flex-col items-center">
<cover-view
class="bg-white w-80 h-80 rounded-full flex items-center justify-center relative"
>
<cover-image
:src="
isVoice
? 'https://oss-lock.xhjcn.ltd/mp/icon_microphone.png'
: 'https://oss-lock.xhjcn.ltd/mp/icon_no_microphone.png'
"
class="w-55 h-55"
></cover-image>
<cover-view
@click="toggleVoice"
class="absolute top-0 left-0 w-full h-full"
></cover-view>
</cover-view>
<cover-view class="mt-2 text-center whitespace-nowrap text-xs">{{
isVoice ? '点击停止' : '点击说话'
}}</cover-view>
</cover-view>
<cover-view class="flex flex-col items-center">
<cover-view
class="bg-[#eb292b] w-80 h-80 rounded-full flex items-center justify-center relative"
>
<cover-image
src="https://oss-lock.xhjcn.ltd/mp/icon_hang_up.png"
class="w-60 h-60"
></cover-image>
<cover-view
@click="handleHangUp"
class="absolute top-0 left-0 w-full h-full"
></cover-view>
</cover-view>
<cover-view class="mt-2 text-center whitespace-nowrap text-xs">挂断</cover-view>
</cover-view>
<cover-view class="flex flex-col items-center">
<cover-view
class="bg-[#63b8af] w-80 h-80 rounded-full flex items-center justify-center relative"
>
<cover-image
src="https://oss-lock.xhjcn.ltd/mp/icon_lock_white.png"
class="w-60 h-60"
></cover-image>
<cover-view
@click="handleLock"
class="absolute top-0 left-0 w-full h-full"
></cover-view>
</cover-view>
<cover-view class="mt-2 text-center whitespace-nowrap text-xs">开锁</cover-view>
</cover-view>
</cover-view>
</cover-view>
<view
v-if="!isVideoLoaded"
class="fixed bottom-[calc(48rpx+env(safe-area-inset-bottom))] w-full flex justify-center"
>
<up-loading-icon
size="70rpx"
:vertical="true"
textSize="28rpx"
text="连接中"
mode="circle"
></up-loading-icon>
</view>
</view> </view>
</template> </template>
@ -250,7 +254,7 @@
return data return data
}) })
const cleanupResources = () => { const cleanupResources = async () => {
if (cleanupCalled.value) return if (cleanupCalled.value) return
cleanupCalled.value = true cleanupCalled.value = true
@ -263,8 +267,8 @@
} }
releaseRecord() releaseRecord()
if (deviceInfo.value) { if (deviceInfo.value) {
stopSendServiceFunction(`${deviceInfo.value.productId}/${deviceInfo.value.deviceName}`) await stopSendServiceFunction(`${deviceInfo.value.productId}/${deviceInfo.value.deviceName}`)
stopServiceFunction(`${deviceInfo.value.productId}/${deviceInfo.value.deviceName}`) await stopServiceFunction(`${deviceInfo.value.productId}/${deviceInfo.value.deviceName}`)
} }
// #endif // #endif
// #ifdef MP-WEIXIN // #ifdef MP-WEIXIN
@ -545,8 +549,15 @@
// #endif // #endif
} }
const callback = audioData => { const callback = async audioData => {
dataSendFunction(`${deviceInfo.value.productId}/${deviceInfo.value.deviceName}`, audioData) const result = await dataSendFunction(
`${deviceInfo.value.productId}/${deviceInfo.value.deviceName}`,
audioData
)
console.log(
`数据传输结果:${result?.data?.result ? result?.data?.result : result?.data?.dynamicJSONFields?.result}`,
audioData
)
} }
const handlePlaySuccess = () => { const handlePlaySuccess = () => {

View File

@ -51,7 +51,6 @@
:defaultIndex="picker" :defaultIndex="picker"
title="选择时间" title="选择时间"
keyName="name" keyName="name"
:itemHeight="70"
:visibleItemCount="5" :visibleItemCount="5"
@close="show = false" @close="show = false"
@cancel="show = false" @cancel="show = false"

View File

@ -68,7 +68,6 @@
:show="showRecordingTime" :show="showRecordingTime"
:columns="recordingTimeList" :columns="recordingTimeList"
keyName="name" keyName="name"
:itemHeight="70"
title="有人出现时录像" title="有人出现时录像"
:visibleItemCount="5" :visibleItemCount="5"
:defaultIndex="[recordingTimeIndex]" :defaultIndex="[recordingTimeIndex]"
@ -80,7 +79,6 @@
:show="showDetectionDistance" :show="showDetectionDistance"
:columns="detectionDistanceList" :columns="detectionDistanceList"
title="人体侦测距离" title="人体侦测距离"
:itemHeight="70"
:visibleItemCount="5" :visibleItemCount="5"
keyName="name" keyName="name"
:defaultIndex="[detectionDistanceIndex]" :defaultIndex="[detectionDistanceIndex]"

View File

@ -61,7 +61,6 @@
:defaultIndex="[distanceIndex]" :defaultIndex="[distanceIndex]"
title="感应距离" title="感应距离"
keyName="name" keyName="name"
:itemHeight="70"
:visibleItemCount="5" :visibleItemCount="5"
@close="showDistance = false" @close="showDistance = false"
@cancel="showDistance = false" @cancel="showDistance = false"
@ -73,7 +72,6 @@
:defaultIndex="[enErrUnlock]" :defaultIndex="[enErrUnlock]"
title="防误开" title="防误开"
keyName="name" keyName="name"
:itemHeight="70"
:visibleItemCount="5" :visibleItemCount="5"
@close="showEnErrUnlock = false" @close="showEnErrUnlock = false"
@cancel="showEnErrUnlock = false" @cancel="showEnErrUnlock = false"

View File

@ -267,6 +267,52 @@
} }
} }
} }
if (data[i].openLockType === 6) {
for (let j = 0; j < data[i].keys.length; j++) {
data[i].keys[j].openLockType = data[i].openLockType
data[i].keys[j].name = data[i].keys[j].palmVeinName
data[i].keys[j].id = data[i].keys[j].palmVeinId
if (data[i].keys[j].palmVeinType === 1) {
data[i].keys[j].timeText =
timeFormat(data[i].keys[j].createDate, 'yyyy-mm-dd hh:MM') + ' 永久'
} else if (data[i].keys[j].palmVeinType === 2) {
data[i].keys[j].timeText =
timeFormat(data[i].keys[j].startDate, 'yyyy-mm-dd hh:MM') +
' - ' +
timeFormat(data[i].keys[j].endDate, 'yyyy-mm-dd hh:MM') +
' 限时'
} else {
data[i].keys[j].timeText =
timeFormat(data[i].keys[j].startDate, 'yyyy-mm-dd') +
' - ' +
timeFormat(data[i].keys[j].endDate, 'yyyy-mm-dd') +
' 循环'
}
}
}
if (data[i].openLockType === 7) {
for (let j = 0; j < data[i].keys.length; j++) {
data[i].keys[j].openLockType = data[i].openLockType
data[i].keys[j].name = data[i].keys[j].remoteName
data[i].keys[j].id = data[i].keys[j].remoteId
if (data[i].keys[j].remoteType === 1) {
data[i].keys[j].timeText =
timeFormat(data[i].keys[j].createDate, 'yyyy-mm-dd hh:MM') + ' 永久'
} else if (data[i].keys[j].remoteType === 2) {
data[i].keys[j].timeText =
timeFormat(data[i].keys[j].startDate, 'yyyy-mm-dd hh:MM') +
' - ' +
timeFormat(data[i].keys[j].endDate, 'yyyy-mm-dd hh:MM') +
' 限时'
} else {
data[i].keys[j].timeText =
timeFormat(data[i].keys[j].startDate, 'yyyy-mm-dd') +
' - ' +
timeFormat(data[i].keys[j].endDate, 'yyyy-mm-dd') +
' 循环'
}
}
}
result.push(...data[i].keys) result.push(...data[i].keys)
} }
console.log(result) console.log(result)

View File

@ -61,7 +61,6 @@
<up-picker <up-picker
:show="show" :show="show"
:columns="columns" :columns="columns"
:itemHeight="70"
:visibleItemCount="5" :visibleItemCount="5"
:defaultIndex="[value - 1]" :defaultIndex="[value - 1]"
@close="show = false" @close="show = false"

View File

@ -83,7 +83,6 @@
<up-datetime-picker <up-datetime-picker
:hasInput="false" :hasInput="false"
:show="showStartTime" :show="showStartTime"
:itemHeight="70"
:visibleItemCount="5" :visibleItemCount="5"
v-model="defaultStartTime" v-model="defaultStartTime"
mode="time" mode="time"
@ -95,7 +94,6 @@
<up-datetime-picker <up-datetime-picker
:hasInput="false" :hasInput="false"
:show="showEndTime" :show="showEndTime"
:itemHeight="70"
:visibleItemCount="5" :visibleItemCount="5"
v-model="defaultEndTime" v-model="defaultEndTime"
mode="time" mode="time"

View File

@ -446,6 +446,10 @@
pending.value = false pending.value = false
uni.hideLoading() uni.hideLoading()
if (code === 0) { if (code === 0) {
if (key === 'appUnlockOnline') {
$bluetooth.currentLockInfo.appUnlockOnline =
$bluetooth.currentLockSetting.lockSettingInfo[key] === 1 ? 0 : 1
}
$bluetooth.updateCurrentLockSetting({ $bluetooth.updateCurrentLockSetting({
...$bluetooth.currentLockSetting, ...$bluetooth.currentLockSetting,
lockSettingInfo: { lockSettingInfo: {

View File

@ -51,7 +51,6 @@
uid: $user.userInfo.uid.toString(), uid: $user.userInfo.uid.toString(),
countReq: 10 countReq: 10
}) })
console.log('code value :', code)
if (code === 0) { if (code === 0) {
if (typeList[progress.value] === 'setting') { if (typeList[progress.value] === 'setting') {
const { code: resultCode } = await lockDataUploadRequest({ const { code: resultCode } = await lockDataUploadRequest({
@ -59,13 +58,12 @@
uploadType: 1, uploadType: 1,
records: data.list records: data.list
}) })
uni.hideLoading()
pending.value = false
$bluetooth.closeBluetoothConnection() $bluetooth.closeBluetoothConnection()
if (resultCode === 0) { if (resultCode === 0) {
const { code, data } = await getLockSettingRequest({ const { code, data } = await getLockSettingRequest({
lockId: $bluetooth.currentLockInfo.lockId lockId: $bluetooth.currentLockInfo.lockId
}) })
pending.value = false
if (code === 0) { if (code === 0) {
if (data.lockSettingInfo.catEyeConfig[0]) { if (data.lockSettingInfo.catEyeConfig[0]) {
data.lockSettingInfo.catEyeConfig[0].catEyeModeConfig.recordTime = Number( data.lockSettingInfo.catEyeConfig[0].catEyeModeConfig.recordTime = Number(
@ -77,19 +75,31 @@
} }
$bluetooth.updateCurrentLockSetting(data) $bluetooth.updateCurrentLockSetting(data)
} }
uni.hideLoading()
uni.showToast({ uni.showToast({
title: '上传成功', title: '上传成功',
icon: 'none' icon: 'none'
}) })
} else { } else {
pending.value = false
uni.hideLoading()
uni.showToast({ uni.showToast({
title: '上传失败,请重试', title: '上传失败,请重试',
icon: 'none' icon: 'none'
}) })
} }
progress.value = 0
page.value = 0
list.value = []
return return
} }
list.value = list.value.concat(data.list) list.value = list.value.concat(data.list)
if (list.value.length === 0) {
page.value = 0
list.value = []
progress.value++
return await asyncData(true)
}
if (data.size === 10) { if (data.size === 10) {
page.value++ page.value++
} else { } else {
@ -107,6 +117,9 @@
title: '上传失败,请重试', title: '上传失败,请重试',
icon: 'none' icon: 'none'
}) })
progress.value = 0
page.value = 0
list.value = []
return return
} }
page.value = 0 page.value = 0
@ -118,6 +131,9 @@
pending.value = false pending.value = false
$bluetooth.closeBluetoothConnection() $bluetooth.closeBluetoothConnection()
uni.hideLoading() uni.hideLoading()
progress.value = 0
page.value = 0
list.value = []
} else { } else {
pending.value = false pending.value = false
$bluetooth.closeBluetoothConnection() $bluetooth.closeBluetoothConnection()
@ -126,6 +142,9 @@
title: '上传失败,请保持在锁附近', title: '上传失败,请保持在锁附近',
icon: 'none' icon: 'none'
}) })
progress.value = 0
page.value = 0
list.value = []
} }
} }
</script> </script>

View File

@ -58,7 +58,6 @@
title="开始时间" title="开始时间"
mode="time" mode="time"
:defaultIndex="startIndex" :defaultIndex="startIndex"
:itemHeight="70"
:visibleItemCount="5" :visibleItemCount="5"
@close="showStartTime = false" @close="showStartTime = false"
@cancel="showStartTime = false" @cancel="showStartTime = false"
@ -70,7 +69,6 @@
title="结束时间" title="结束时间"
mode="time" mode="time"
:defaultIndex="endIndex" :defaultIndex="endIndex"
:itemHeight="70"
:visibleItemCount="5" :visibleItemCount="5"
@close="showEndTime = false" @close="showEndTime = false"
@cancel="showEndTime = false" @cancel="showEndTime = false"

View File

@ -94,10 +94,10 @@
abbreviation: 'CN', abbreviation: 'CN',
group: 'Z' group: 'Z'
}) })
const username = ref('') const username = ref('18174429647')
const password = ref('') const password = ref('..022059')
const select = ref(false) const select = ref(true)
const pending = ref(false) const pending = ref(false)

View File

@ -1,6 +1,6 @@
<template> <template>
<view> <view>
<view class="tips">找回密码和登录新设备时可通过绑定的邮箱验证</view> <view class="tips">在APP上找回密码和登录新设备时可通过绑定的邮箱验证</view>
<input <input
class="input-email" class="input-email"
:value="email" :value="email"

View File

@ -719,7 +719,6 @@ export const useBluetoothStore = defineStore('ble', {
list: decrypted.slice(9, length) list: decrypted.slice(9, length)
} }
} }
console.log(1234, cmdId, data)
characteristicValueCallback({ characteristicValueCallback({
code: decrypted[2], code: decrypted[2],
data data

View File

@ -1,3 +1,4 @@
{ {
"minSdkVersion": "21" "minSdkVersion": "21",
"dependencies": ["com.tencent.iot.thirdparty.android:media-server:1.0.7"]
} }

View File

@ -5,51 +5,76 @@ import 'android.media.AudioRecord'
import 'android.media.MediaRecorder' import 'android.media.MediaRecorder'
import 'android.media.AudioFormat' import 'android.media.AudioFormat'
import 'android.media.MediaSyncEvent' import 'android.media.MediaSyncEvent'
import 'android.media.MediaCodec'
import 'android.media.MediaCodecInfo'
import 'android.media.MediaFormat'
import 'java.lang.Thread' import 'java.lang.Thread'
import { UTSAndroid } from 'io.dcloud.uts' import { UTSAndroid } from 'io.dcloud.uts'
import { Result } from '../interface.uts' import { Result } from '../interface.uts'
import { FLVPacker, FLVListener } from 'com.tencent.iot.thirdparty.flv'
let recorder: AudioRecord | null = null let recorder: AudioRecord | null = null
let recorderState: boolean = false let aacEncoder: MediaCodec | null = null
let flvPacker: FLVPacker | null = null
let recordThread: Thread | null = null
let isRecording: boolean = false
let bufferSizeInBytes: Int = 0 let bufferSizeInBytes: Int = 0
class MyFLVListener extends FLVListener {
private callback: (data: Array<number>) => void
constructor(callback: (data: Array<number>) => void) {
super()
this.callback = callback
}
override onFLV(data: ByteArray) {
if (isRecording) {
const numberArray: Array<number> = []
for (let i: Int = 0; i < data.size; i++) {
numberArray.push(data[i].toInt())
}
UTSAndroid.getUniActivity()?.runOnUiThread(() => {
this.callback(numberArray)
})
}
}
}
function addADTStoPacket(packet: ByteArray, packetLen: Int) {
const profile = 2
const freqIdx = 8
const chanCfg = 1
packet[0] = (0xff).toByte()
packet[1] = (0xf9).toByte()
packet[2] = (((profile - 1) << 6) | (freqIdx << 2) | (chanCfg >> 2)).toByte()
packet[3] = (((chanCfg & 3) << 6) | (packetLen >> 11)).toByte()
packet[4] = ((packetLen & 0x7ff) >> 3).toByte()
packet[5] = (((packetLen & 7) << 5) | 0x1f).toByte()
packet[6] = (0xfc).toByte()
}
export const requestPermission = async function (): Promise<Result> { export const requestPermission = async function (): Promise<Result> {
return new Promise(resolve => { return new Promise(resolve => {
try { try {
let permissionNeed = ['android.permission.RECORD_AUDIO'] const permissionNeed = ['android.permission.RECORD_AUDIO']
UTSAndroid.requestSystemPermission( UTSAndroid.requestSystemPermission(
UTSAndroid.getUniActivity()!, UTSAndroid.getUniActivity()!,
permissionNeed, permissionNeed,
function (allRight: boolean, _: string[]) { (allRight: boolean, _: string[]) => {
if (allRight) { if (allRight) {
resolve({ resolve({ code: 0, data: {}, message: '成功' })
code: 0,
data: {},
message: '成功'
})
} else { } else {
resolve({ resolve({ code: -1, data: {}, message: '失败' })
code: -1,
data: {},
message: '失败'
})
} }
}, },
function (_: boolean, _: string[]) { (_: boolean, _: string[]) => {
resolve({ resolve({ code: -1, data: {}, message: '失败' })
code: -1,
data: {},
message: '失败'
})
} }
) )
} catch (error) { } catch (error) {
resolve({ resolve({ code: -1, data: {}, message: error.toString() })
code: -1,
data: {},
message: error.toString()
})
} }
}) })
} }
@ -57,7 +82,7 @@ export const requestPermission = async function (): Promise<Result> {
export const initAudio = async function (): Promise<Result> { export const initAudio = async function (): Promise<Result> {
try { try {
const audioSource = MediaRecorder.AudioSource.MIC const audioSource = MediaRecorder.AudioSource.MIC
const sampleRateInHz = 44100 const sampleRateInHz = 16000
const channelConfig = AudioFormat.CHANNEL_IN_MONO const channelConfig = AudioFormat.CHANNEL_IN_MONO
const audioFormat = AudioFormat.ENCODING_PCM_16BIT const audioFormat = AudioFormat.ENCODING_PCM_16BIT
@ -67,8 +92,6 @@ export const initAudio = async function (): Promise<Result> {
audioFormat audioFormat
) )
console.log('bufferSizeInBytes', bufferSizeInBytes)
recorder = new AudioRecord( recorder = new AudioRecord(
audioSource, audioSource,
sampleRateInHz.toInt(), sampleRateInHz.toInt(),
@ -77,133 +100,163 @@ export const initAudio = async function (): Promise<Result> {
bufferSizeInBytes bufferSizeInBytes
) )
const currentRecorder = recorder if (recorder?.getState() == AudioRecord.STATE_INITIALIZED) {
if (currentRecorder !== null) { return { code: 0, data: {}, message: '成功' }
console.log('初始化录音结果:', currentRecorder.getState()) } else {
if (currentRecorder.getState() == AudioRecord.STATE_INITIALIZED) { return { code: -1, data: {}, message: '初始化录音失败' }
return {
code: 0,
data: {},
message: '成功'
}
}
}
return {
code: -1,
data: {},
message: '初始化录音失败'
} }
} catch (error) { } catch (error) {
console.log('初始化录音失败', error) return { code: -1, data: {}, message: error.toString() }
return {
code: -1,
data: {},
message: error.toString()
}
} }
} }
export async function onStartRecord(callback: (data: Array) => void): Promise<Result> { export function onStartRecord(callback: (data: Array<number>) => void) {
try { stopRecord()
const callbackFunction = () => { .then(() => {
const currentRecorder = recorder const currentRecorder = recorder
if (currentRecorder !== null) { if (currentRecorder == null) {
while (recorderState) { return
let audioData = new ByteArray(bufferSizeInBytes) }
const result: Int = currentRecorder.read(audioData, 0, bufferSizeInBytes)
if (result > 0) { const listener = new MyFLVListener(callback)
callback(Array.fromNative(audioData)) flvPacker = new FLVPacker(listener, true, false)
console.log('录音按帧返回数据', Array.fromNative(audioData))
const sampleRateInHz = 16000
const channelCount = 1
const bitRate = 96000
aacEncoder = MediaCodec.createEncoderByType('audio/mp4a-latm')
const mediaFormat = MediaFormat.createAudioFormat(
'audio/mp4a-latm',
sampleRateInHz.toInt(),
channelCount.toInt()
)
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitRate.toInt())
mediaFormat.setInteger(
MediaFormat.KEY_AAC_PROFILE,
MediaCodecInfo.CodecProfileLevel.AACObjectLC
)
mediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 1024 * 1024)
aacEncoder!!.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
recordThread = new Thread(() => {
try {
if (aacEncoder == null || currentRecorder == null || flvPacker == null) {
return
} }
Thread.sleep(10)
currentRecorder.startRecording()
aacEncoder!!.start()
isRecording = true
const audioInfo = new MediaCodec.BufferInfo()
while (isRecording) {
const inputBufferId = aacEncoder!!.dequeueInputBuffer(10000)
if (inputBufferId >= 0) {
const inputBuffer = aacEncoder!!.getInputBuffer(inputBufferId)!!
const readSize = currentRecorder.read(inputBuffer, bufferSizeInBytes)
if (readSize > 0 && isRecording) {
aacEncoder!!.queueInputBuffer(
inputBufferId,
0,
readSize,
(Date.now() * 1000).toLong(),
0
)
}
}
var outputBufferId = aacEncoder!!.dequeueOutputBuffer(audioInfo, 10000)
while (outputBufferId >= 0 && isRecording) {
const outputBuffer = aacEncoder!!.getOutputBuffer(outputBufferId)!!
if (audioInfo.size > 0 && flvPacker != null) {
const outDataSize = audioInfo.size
const aacPacketWithAdts = ByteArray(outDataSize + 7)
addADTStoPacket(aacPacketWithAdts, outDataSize + 7)
outputBuffer.position(audioInfo.offset)
outputBuffer.limit(audioInfo.offset + outDataSize)
outputBuffer.get(aacPacketWithAdts, 7, outDataSize)
if (flvPacker != null && isRecording) {
flvPacker!!.encodeFlv(
aacPacketWithAdts,
FLVPacker.TYPE_AUDIO,
Date.now().toLong()
)
}
}
aacEncoder!!.releaseOutputBuffer(outputBufferId, false)
outputBufferId = aacEncoder!!.dequeueOutputBuffer(audioInfo, 0)
}
}
} catch (error) {
console.log('Record thread error: ' + error.toString())
} finally {
try {
currentRecorder?.stop()
} catch (e) {}
try {
aacEncoder?.stop()
aacEncoder?.release()
} catch (e) {}
try {
flvPacker?.release()
} catch (e) {}
aacEncoder = null
flvPacker = null
isRecording = false
} }
} })
return recordThread!!.start()
} })
.catch(error => {
const currentRecorder = recorder console.log('Error in stopRecord(): ' + error.toString())
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> { export const stopRecord = async function (): Promise<Result> {
try { try {
const currentRecorder = recorder if (isRecording) {
if (currentRecorder !== null) { isRecording = false
currentRecorder.stop()
recorderState = false if (recordThread != null) {
console.log('停止录音') try {
return { recordThread!!.join(1000)
code: 0, } catch (e) {
data: {}, console.log('recordThread.join error: ' + e.toString())
message: '成功' }
recordThread = null
} }
return { code: 0, data: {}, message: '成功' }
} }
return { return { code: 0, data: {}, message: '录音未开始' }
code: -1,
data: {},
message: '停止录音失败'
}
} catch (error) { } catch (error) {
console.log('停止录音失败', error) isRecording = false
recorderState = false recordThread = null
return { return { code: -1, data: {}, message: error.toString() }
code: -1,
data: {},
message: error.toString()
}
} }
} }
export const releaseRecord = async function (): Promise<Result> { export const releaseRecord = async function (): Promise<Result> {
try { try {
await stopRecord()
const currentRecorder = recorder const currentRecorder = recorder
if (currentRecorder !== null) { if (currentRecorder != null) {
currentRecorder.release() try {
console.log('释放录音') if (currentRecorder.getState() == AudioRecord.STATE_INITIALIZED) {
return { currentRecorder.release()
code: 0, }
data: {}, } catch (e) {}
message: '成功' recorder = null
}
}
return {
code: -1,
data: {},
message: '释放录音失败'
} }
return { code: 0, data: {}, message: '成功' }
} catch (error) { } catch (error) {
console.log('释放录音失败', error) return { code: -1, data: {}, message: error.toString() }
return {
code: -1,
data: {},
message: error.toString()
}
} }
} }
// @ts-ignore-end // @ts-ignore-end

View File

@ -1,236 +1,398 @@
import Foundation
import AVFoundation import AVFoundation
import CoreMedia
import Foundation
@objc @objc(RecordPermission)
public class RecordPermission: NSObject { public class RecordPermission: NSObject {
@objc @objc
public static func requestRecordPermission(_ completion: @escaping (Bool) -> Void) { public static func requestRecordPermission(_ callback: @escaping (Bool) -> Void) {
AVAudioSession.sharedInstance().requestRecordPermission { granted in AVAudioSession.sharedInstance().requestRecordPermission { granted in
completion(granted) callback(granted)
} }
} }
} }
@objc @objc(AudioRecorderManager)
public class AudioRecorderManager: NSObject { public class AudioRecorderManager: NSObject {
@objc public static let shared = AudioRecorderManager()
private enum State {
case idle, recording, stopped
}
private var state: State = .idle
private let processingQueue = DispatchQueue(label: "com.xhjcn.lock.lite.audio.processing.queue")
private var recordingCallback: ((Data?, Bool, String) -> Void)?
private var audioEngine: AVAudioEngine? private var audioEngine: AVAudioEngine?
private var audioConverter: AVAudioConverter? private var audioConverter: AVAudioConverter?
private var aacBuffer: AVAudioCompressedBuffer? private var outputBuffer: AVAudioCompressedBuffer?
private var flvMuxer: FLVMuxer?
private let lock = NSLock() @objc
private var _isRecording = false public func initAudio(_ callback: @escaping (Bool, String) -> Void) {
var isRecording: Bool { callback(true, "init success")
get { }
lock.lock()
defer { lock.unlock() } @objc
return _isRecording public func startRecord(_ callback: @escaping (Data?, Bool, String) -> Void) {
} processingQueue.async { [weak self] in
set { guard let self = self else { return }
lock.lock()
_isRecording = newValue guard self.state == .idle || self.state == .stopped else {
lock.unlock() self.dispatchError(callback: callback, message: "Recording is already in progress.")
return
}
self.recordingCallback = callback
if AVAudioSession.sharedInstance().recordPermission != .granted {
self.dispatchError(callback: callback, message: "Microphone permission not granted.")
return
}
do {
try self.setupAudioSession()
try self.setupAudioEngine()
guard let converter = self.audioConverter else {
self.cleanUp()
self.dispatchError(callback: callback, message: "Failed to setup audio engine components.")
return
}
self.flvMuxer = FLVMuxer(sampleRate: converter.outputFormat.sampleRate, channels: Double(converter.outputFormat.channelCount))
var initialData = Data()
let header = self.flvMuxer!.getHeader()
initialData.append(header)
let metaTag = self.flvMuxer!.getMetaTag()
initialData.append(metaTag)
let audioConfigTag = self.flvMuxer!.getAudioSpecificConfigTag(config: nil)
initialData.append(audioConfigTag)
let inputNode = self.audioEngine!.inputNode
let inputFormat = inputNode.outputFormat(forBus: 0)
inputNode.installTap(onBus: 0, bufferSize: 1024, format: inputFormat) { [weak self] (buffer, _) in
self?.processAudioBuffer(buffer)
}
DispatchQueue.main.async {
self.dispatchData(initialData)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.processingQueue.async { [weak self] in
guard let self = self, let engine = self.audioEngine else { return }
do {
engine.prepare()
try engine.start()
self.state = .recording
self.dispatchSuccess(message: "Recording started successfully.")
} catch {
self.cleanUp()
self.dispatchError(callback: self.recordingCallback ?? {_,_,_ in }, message: "Failed to start engine: \(error.localizedDescription)")
}
}
}
}
} catch {
self.cleanUp()
self.dispatchError(callback: callback, message: "Failed to start recording: \(error.localizedDescription)")
}
} }
} }
@objc @objc
public static let shared = AudioRecorderManager() public func stopRecord(_ callback: @escaping (Bool, String, String) -> Void) {
processingQueue.async { [weak self] in
private override init() {} guard let self = self else { return }
guard self.state == .recording else {
@objc callback(false, "Not recording.", "")
public func initAudio(_ completion: @escaping (Bool, String) -> Void) { return
completion(true, "Module initialized") }
self.cleanUp()
self.state = .stopped
callback(true, "Recording stopped.", "")
}
} }
@objc @objc
public func startRecord(_ completion: @escaping (Data?, Bool, String) -> Void) { public func releaseRecord(_ callback: @escaping (Bool, String) -> Void) {
if self.isRecording { processingQueue.async { [weak self] in
completion(nil, false, "Recording is already in progress.") guard let self = self else { return }
return self.cleanUp()
self.state = .idle
callback(true, "Record released.")
} }
}
let session = AVAudioSession.sharedInstance() private func setupAudioSession() throws {
do { let audioSession = AVAudioSession.sharedInstance()
try session.setCategory(.playAndRecord, mode: .default, options: .defaultToSpeaker) try audioSession.setCategory(.playAndRecord, mode: .voiceChat, options: [.defaultToSpeaker, .allowBluetooth])
try session.setPreferredSampleRate(16000.0) try audioSession.setActive(true)
try session.setPreferredInputNumberOfChannels(1) }
try session.setActive(true)
} catch {
completion(nil, false, "Failed to set up audio session: \(error.localizedDescription)")
return
}
private func setupAudioEngine() throws {
audioEngine = AVAudioEngine() audioEngine = AVAudioEngine()
guard let audioEngine = audioEngine else { guard let engine = audioEngine else {
completion(nil, false, "Failed to create audio engine") throw NSError(domain: "AudioRecorderManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to create AVAudioEngine"])
return
} }
let inputNode = audioEngine.inputNode let inputNode = engine.inputNode
let inputFormat = inputNode.outputFormat(forBus: 0) let inputFormat = inputNode.outputFormat(forBus: 0)
var outputFormatDescription = AudioStreamBasicDescription( guard inputFormat.sampleRate > 0 else {
mSampleRate: 16000.0, throw NSError(domain: "AudioRecorderManager", code: -2, userInfo: [NSLocalizedDescriptionKey: "Input node has an invalid sample rate."])
mFormatID: kAudioFormatMPEG4AAC, }
mFormatFlags: 2,
mBytesPerPacket: 0,
mFramesPerPacket: 1024,
mBytesPerFrame: 0,
mChannelsPerFrame: 1,
mBitsPerChannel: 0,
mReserved: 0
)
guard let outputFormat = AVAudioFormat(streamDescription: &outputFormatDescription) else { let outputFormatSettings: [String: Any] = [
completion(nil, false, "Failed to create output audio format") AVFormatIDKey: kAudioFormatMPEG4AAC,
return AVSampleRateKey: 16000,
AVNumberOfChannelsKey: inputFormat.channelCount,
AVEncoderBitRateKey: 48000,
AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
]
guard let outputFormat = AVAudioFormat(settings: outputFormatSettings) else {
throw NSError(domain: "AudioRecorderManager", code: -3, userInfo: [NSLocalizedDescriptionKey: "Failed to create output format."])
} }
guard let converter = AVAudioConverter(from: inputFormat, to: outputFormat) else { guard let converter = AVAudioConverter(from: inputFormat, to: outputFormat) else {
completion(nil, false, "Failed to create audio converter") throw NSError(domain: "AudioRecorderManager", code: -4, userInfo: [NSLocalizedDescriptionKey: "Failed to create audio converter."])
return
} }
converter.bitRate = 48000
converter.bitRateStrategy = AVAudioBitRateStrategy_Constant
self.audioConverter = converter self.audioConverter = converter
self.aacBuffer = AVAudioCompressedBuffer( self.outputBuffer = AVAudioCompressedBuffer(
format: outputFormat, format: converter.outputFormat,
packetCapacity: 1, packetCapacity: 1,
maximumPacketSize: converter.maximumOutputPacketSize 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) { private func processAudioBuffer(_ pcmBuffer: AVAudioPCMBuffer) {
guard let converter = self.audioConverter, let outputBuffer = self.aacBuffer else { return } guard state == .recording,
let converter = self.audioConverter,
let outputBuffer = self.outputBuffer,
let flvMuxer = self.flvMuxer else { return }
outputBuffer.byteLength = 0
outputBuffer.packetCount = 0 outputBuffer.packetCount = 0
outputBuffer.byteLength = 0
var error: NSError? var error: NSError?
var pcmBufferWasProvided = false
let status = converter.convert(to: outputBuffer, error: &error) { _, outStatus in let status = converter.convert(to: outputBuffer, error: &error) { _, outStatus in
if pcmBufferWasProvided {
outStatus.pointee = .noDataNow
return nil
}
outStatus.pointee = .haveData outStatus.pointee = .haveData
pcmBufferWasProvided = true
return pcmBuffer return pcmBuffer
} }
guard status != .error, error == nil else { if status == .haveData, outputBuffer.byteLength > 0 {
print("AAC conversion error: \(error?.localizedDescription ?? "unknown")") let aacData = Data(bytes: outputBuffer.data, count: Int(outputBuffer.byteLength))
return let aacTag = flvMuxer.getAACTag(data: aacData)
dispatchData(aacTag)
} else if let error = error {
processingQueue.async { [weak self] in
guard let self = self, self.state == .recording else { return }
let errorMessage = "AAC Conversion Error: \(error.localizedDescription)"
self.dispatchError(callback: self.recordingCallback ?? { _, _, _ in }, message: errorMessage)
self.cleanUp()
self.state = .stopped
}
} }
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]? { private func cleanUp() {
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?.stop()
audioEngine?.inputNode.removeTap(onBus: 0) audioEngine?.inputNode.removeTap(onBus: 0)
try? AVAudioSession.sharedInstance().setActive(false)
audioEngine = nil audioEngine = nil
audioConverter = nil audioConverter = nil
aacBuffer = nil outputBuffer = nil
flvMuxer = nil
do { recordingCallback = nil
try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
} catch {
print("Failed to deactivate audio session: \(error)")
}
completion(true, "Recording stopped", "")
} }
@objc private func dispatchData(_ data: Data) {
public func releaseRecord(_ completion: @escaping (Bool, String) -> Void) { DispatchQueue.main.async { [weak self] in
if self.isRecording { self?.recordingCallback?(data, false, "")
self.isRecording = false
audioEngine?.stop()
audioEngine?.inputNode.removeTap(onBus: 0)
} }
audioEngine = nil }
audioConverter = nil
aacBuffer = nil
do { private func dispatchError(callback: @escaping (Data?, Bool, String) -> Void, message: String) {
try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation) DispatchQueue.main.async {
} catch { callback(nil, false, message)
print("Failed to deactivate audio session on release: \(error)")
} }
}
completion(true, "Record released") private func dispatchSuccess(message: String) {
DispatchQueue.main.async { [weak self] in
self?.recordingCallback?(nil, true, message)
}
} }
} }
@objc private class FLVMuxer {
private var startTime: CFTimeInterval
private var sampleRate: Double
private var channels: Double
init(sampleRate: Double = 44100.0, channels: Double = 1.0) {
self.startTime = CACurrentMediaTime()
self.sampleRate = sampleRate
self.channels = channels
}
func getHeader() -> Data {
return Data([
0x46, 0x4C, 0x56,
0x01,
0x04,
0x00, 0x00, 0x00, 0x09,
0x00, 0x00, 0x00, 0x00
])
}
func getMetaTag() -> Data {
var scriptData = Data()
scriptData.append(amfString("onMetaData"))
let properties: [(String, Any)] = [
("audiocodecid", 10.0),
("audiosamplerate", self.sampleRate),
("audiochannels", self.channels),
("stereo", self.channels > 1),
("creator", "starlock-record")
]
scriptData.append(amfECMAArray(properties))
let tag = createTag(type: .script, timestamp: 0, body: scriptData)
return tag
}
func getAudioSpecificConfigTag(config: Data?) -> Data {
let audioObjectType: UInt8 = 2
guard let frequencyIndex = getSamplingFrequencyIndex(sampleRate: self.sampleRate) else {
let defaultConfig = Data([0x12, 0x08])
let body = Data([0xAE, 0x00]) + defaultConfig
return createTag(type: .audio, timestamp: 0, body: body)
}
let channelConfig = UInt8(self.channels)
var configByte1: UInt8 = 0
configByte1 |= (audioObjectType << 3)
configByte1 |= (frequencyIndex >> 1)
var configByte2: UInt8 = 0
configByte2 |= ((frequencyIndex & 0x01) << 7)
configByte2 |= (channelConfig << 3)
let audioSpecificConfig = Data([configByte1, configByte2])
let body = Data([0xAE, 0x00]) + audioSpecificConfig
let tag = createTag(type: .audio, timestamp: 0, body: body)
return tag
}
func getAACTag(data: Data) -> Data {
let elapsed = CACurrentMediaTime() - self.startTime
let timestamp = UInt32(elapsed * 1000)
let body = Data([0xAE, 0x01]) + data
return createTag(type: .audio, timestamp: timestamp, body: body)
}
private func createTag(type: TagType, timestamp: UInt32, body: Data) -> Data {
let dataSize = UInt32(body.count)
let tagHeaderSize: UInt32 = 11
var header = Data(capacity: Int(tagHeaderSize))
header.append(type.rawValue)
header.append(UInt8((dataSize >> 16) & 0xFF))
header.append(UInt8((dataSize >> 8) & 0xFF))
header.append(UInt8(dataSize & 0xFF))
header.append(UInt8((timestamp >> 16) & 0xFF))
header.append(UInt8((timestamp >> 8) & 0xFF))
header.append(UInt8(timestamp & 0xFF))
header.append(UInt8((timestamp >> 24) & 0xFF))
header.append(contentsOf: [0x00, 0x00, 0x00])
var completeTag = Data()
completeTag.append(header)
completeTag.append(body)
let fullTagSize = UInt32(header.count + body.count)
completeTag.append(contentsOf: fullTagSize.bytes)
return completeTag
}
private func getSamplingFrequencyIndex(sampleRate: Double) -> UInt8? {
let rates: [Double: UInt8] = [
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
]
return rates[sampleRate]
}
private enum TagType: UInt8 { case audio = 0x08, video = 0x09, script = 0x12 }
private func amfString(_ str: String) -> Data {
var data = Data([0x02])
let bytes = [UInt8](str.utf8)
data.append(UInt8((bytes.count >> 8) & 0xFF))
data.append(UInt8(bytes.count & 0xFF))
data.append(contentsOf: bytes)
return data
}
private func amfNumber(_ val: Double) -> Data {
var data = Data([0x00])
data.append(contentsOf: val.bitPattern.bytes)
return data
}
private func amfBool(_ val: Bool) -> Data {
return Data([0x01, val ? 0x01 : 0x00])
}
private func amfECMAArray(_ properties: [(String, Any)]) -> Data {
var data = Data([0x08])
let countBytes = UInt32(properties.count).bytes
data.append(contentsOf: countBytes)
for (key, value) in properties {
data.append(amfString(key).dropFirst(1))
if let numValue = value as? Double {
data.append(amfNumber(numValue))
} else if let boolValue = value as? Bool {
data.append(amfBool(boolValue))
} else if let stringValue = value as? String {
data.append(amfString(stringValue))
}
}
data.append(contentsOf: [0x00, 0x00, 0x09])
return data
}
}
fileprivate extension FixedWidthInteger {
var bytes: [UInt8] { withUnsafeBytes(of: self.bigEndian) { Array($0) } }
}
@objc(UTSConversionHelper)
public class UTSConversionHelper: NSObject { public class UTSConversionHelper: NSObject {
@objc @objc
public static func dataToNSArray(_ data: Data) -> NSArray { public static func dataToNSArray(_ data: Data) -> NSArray {

View File

@ -61,7 +61,7 @@ export const stopRecord = async function (): Promise<Result> {
AudioRecorderManager.shared.stopRecord( AudioRecorderManager.shared.stopRecord(
(success: boolean, message: string, filePath: string) => { (success: boolean, message: string, filePath: string) => {
if (success) { if (success) {
resolve({ code: 0, data: { filePath: filePath }, message: message }) resolve({ code: 0, data: {}, message: message })
} else { } else {
resolve({ code: -1, data: {}, message: message }) resolve({ code: -1, data: {}, message: message })
} }

View File

@ -15,28 +15,28 @@ class XP2PCallbackImpl extends XP2PCallback {
} }
override fail(msg: string | null, errorCode: Int): void { override fail(msg: string | null, errorCode: Int): void {
console.log('XP2P callback fail:', msg, errorCode) // console.log('XP2P callback fail:', msg, errorCode)
} }
override commandRequest(id: string | null, msg: string | null): void { override commandRequest(id: string | null, msg: string | null): void {
console.log('XP2P callback commandRequest:', id, msg) // console.log('XP2P callback commandRequest:', id, msg)
} }
override xp2pEventNotify(id: string | null, msg: string | null, event: Int): void { override xp2pEventNotify(id: string | null, msg: string | null, event: Int): void {
console.log('XP2P callback xp2pEventNotify:', id, msg, event) // console.log('XP2P callback xp2pEventNotify:', id, msg, event)
this.eventNotifyCallback?.invoke(id, msg, event) this.eventNotifyCallback?.invoke(id, msg, event)
} }
override avDataRecvHandle(id: string | null, data: ByteArray | null, len: Int): void { override avDataRecvHandle(id: string | null, data: ByteArray | null, len: Int): void {
console.log('XP2P callback avDataRecvHandle:', id, len) // console.log('XP2P callback avDataRecvHandle:', id, len)
} }
override avDataCloseHandle(id: string | null, msg: string | null, errorCode: Int): void { override avDataCloseHandle(id: string | null, msg: string | null, errorCode: Int): void {
console.log('XP2P callback avDataCloseHandle:', id, msg, errorCode) // console.log('XP2P callback avDataCloseHandle:', id, msg, errorCode)
} }
override onDeviceMsgArrived(id: string | null, data: ByteArray | null, len: Int): string { override onDeviceMsgArrived(id: string | null, data: ByteArray | null, len: Int): string {
console.log('XP2P callback onDeviceMsgArrived:', id, len) // console.log('XP2P callback onDeviceMsgArrived:', id, len)
return '' return ''
} }
} }
@ -103,7 +103,8 @@ export const startServiceFunction = async function (
export const stopServiceFunction = async function (id: string): Promise<Result> { export const stopServiceFunction = async function (id: string): Promise<Result> {
try { try {
await XP2P.stopService(id) const result = await XP2P.stopService(id)
console.log('停止服务', result)
return { return {
code: 0, code: 0,
data: {}, data: {},
@ -125,7 +126,7 @@ export const runSendServiceFunction = async function (
): Promise<Result> { ): Promise<Result> {
try { try {
const result = await XP2P.runSendService(id, cmd, crypto) const result = await XP2P.runSendService(id, cmd, crypto)
console.log('开始发送服务', result) console.log('发送服务', result)
return { return {
code: 0, code: 0,
data: {}, data: {},
@ -168,11 +169,11 @@ export const dataSendFunction = async function (id: string, data: Array<number>)
const result = await XP2P.dataSend(id, byteArray, data.length.toInt()) const result = await XP2P.dataSend(id, byteArray, data.length.toInt())
console.log('发送数据结果', result, byteArray)
return { return {
code: 0, code: 0,
data: {}, data: {
result: result
},
message: '成功' message: '成功'
} }
} catch (error) { } catch (error) {

View File

@ -132,7 +132,7 @@ export const runSendServiceFunction = async function (
crypto: boolean crypto: boolean
): Promise<Result> { ): Promise<Result> {
try { try {
const result = runSendService(id, cmd, crypto) const result = await runSendService(id, cmd, crypto)
console.log('开始发送服务', result) console.log('开始发送服务', result)
return { return {
code: 0, code: 0,
@ -178,14 +178,15 @@ export const dataSendFunction = async function (id: string, data: Array<number>)
} }
try { try {
const result = dataSend(id, buffer, data.length) const result = await dataSend(id, buffer, data.length)
// console.log('发送数据结果', result)
XP2PDataHelper.deallocateBytes(buffer) XP2PDataHelper.deallocateBytes(buffer)
return { return {
code: 0, code: 0,
data: {}, data: {
result: result
},
message: '成功' message: '成功'
} }
} catch (error) { } catch (error) {