jessie.huo@dji.com
11 months ago
40 changed files with 2811 additions and 845 deletions
@ -1 +1 @@ |
|||||||
registry=https://registry.npm.taobao.org/ |
registry=https://registry.npmmirror.com/ |
@ -0,0 +1,81 @@ |
|||||||
|
import request from '../http/request' |
||||||
|
import { IWorkspaceResponse } from '../http/type' |
||||||
|
import { EFlightAreaType, ESyncStatus, FlightAreaContent } from './../../types/flight-area' |
||||||
|
import { ELocalStorageKey } from '/@/types/enums' |
||||||
|
import { GeojsonCoordinate } from '/@/utils/genjson' |
||||||
|
|
||||||
|
export interface GetFlightArea { |
||||||
|
area_id: string, |
||||||
|
name: string, |
||||||
|
type: EFlightAreaType, |
||||||
|
content: FlightAreaContent, |
||||||
|
status: boolean, |
||||||
|
username: string, |
||||||
|
create_time: number, |
||||||
|
update_time: number, |
||||||
|
} |
||||||
|
|
||||||
|
export interface PostFlightAreaBody { |
||||||
|
id: string, |
||||||
|
name: string, |
||||||
|
type: EFlightAreaType, |
||||||
|
content: { |
||||||
|
properties: { |
||||||
|
color: string, |
||||||
|
clampToGround: boolean, |
||||||
|
}, |
||||||
|
geometry: { |
||||||
|
type: string, |
||||||
|
coordinates: GeojsonCoordinate | GeojsonCoordinate[][], |
||||||
|
radius?: number, |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export interface FlightAreaStatus { |
||||||
|
sync_code: number, |
||||||
|
sync_status: ESyncStatus, |
||||||
|
sync_msg: string, |
||||||
|
|
||||||
|
} |
||||||
|
export interface GetDeviceStatus { |
||||||
|
device_sn: string, |
||||||
|
nickname?: string, |
||||||
|
device_name?: string, |
||||||
|
online?: boolean, |
||||||
|
flight_area_status: FlightAreaStatus, |
||||||
|
} |
||||||
|
|
||||||
|
const MAP_API_PREFIX = '/map/api/v1' |
||||||
|
|
||||||
|
const workspaceId: string = localStorage.getItem(ELocalStorageKey.WorkspaceId) || '' |
||||||
|
|
||||||
|
export async function getFlightAreaList (): Promise<IWorkspaceResponse<GetFlightArea[]>> { |
||||||
|
const resp = await request.get(`${MAP_API_PREFIX}/workspaces/${workspaceId}/flight-areas`) |
||||||
|
return resp.data |
||||||
|
} |
||||||
|
|
||||||
|
export async function changeFlightAreaStatus (area_id: string, status: boolean): Promise<IWorkspaceResponse<any>> { |
||||||
|
const resp = await request.put(`${MAP_API_PREFIX}/workspaces/${workspaceId}/flight-area/${area_id}`, { status }) |
||||||
|
return resp.data |
||||||
|
} |
||||||
|
|
||||||
|
export async function saveFlightArea (body: PostFlightAreaBody): Promise<IWorkspaceResponse<any>> { |
||||||
|
const resp = await request.post(`${MAP_API_PREFIX}/workspaces/${workspaceId}/flight-area`, body) |
||||||
|
return resp.data |
||||||
|
} |
||||||
|
|
||||||
|
export async function deleteFlightArea (area_id: string): Promise<IWorkspaceResponse<any>> { |
||||||
|
const resp = await request.delete(`${MAP_API_PREFIX}/workspaces/${workspaceId}/flight-area/${area_id}`) |
||||||
|
return resp.data |
||||||
|
} |
||||||
|
|
||||||
|
export async function syncFlightArea (device_sn: string[]): Promise<IWorkspaceResponse<any>> { |
||||||
|
const resp = await request.post(`${MAP_API_PREFIX}/workspaces/${workspaceId}/flight-area/sync`, { device_sn }) |
||||||
|
return resp.data |
||||||
|
} |
||||||
|
|
||||||
|
export async function getDeviceStatus (): Promise<IWorkspaceResponse<GetDeviceStatus[]>> { |
||||||
|
const resp = await request.get(`${MAP_API_PREFIX}/workspaces/${workspaceId}/device-status`) |
||||||
|
return resp.data |
||||||
|
} |
@ -0,0 +1,59 @@ |
|||||||
|
<template> |
||||||
|
<div @click="selectCurrent"> |
||||||
|
<a-dropdown class="height-100 width-100 icon-panel"> |
||||||
|
<FlightAreaIcon :type="actionMap[selectedKey].type" :is-circle="actionMap[selectedKey].isCircle" :hide-title="true"/> |
||||||
|
<template #overlay> |
||||||
|
<a-menu @click="selectAction" mode="vertical-right" :selectedKeys="[selectedKey]"> |
||||||
|
<a-menu-item v-for="(v, k) in actionMap" :key="k"> |
||||||
|
<FlightAreaIcon :type="v.type" :is-circle="v.isCircle"/> |
||||||
|
</a-menu-item> |
||||||
|
</a-menu> |
||||||
|
</template> |
||||||
|
</a-dropdown> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script lang="ts" setup> |
||||||
|
import { ref, defineEmits } from 'vue' |
||||||
|
import { EFlightAreaType } from '../../types/flight-area' |
||||||
|
import FlightAreaIcon from './FlightAreaIcon.vue' |
||||||
|
|
||||||
|
const emit = defineEmits(['select-action', 'click']) |
||||||
|
|
||||||
|
const actionMap: Record<string, { type: EFlightAreaType, isCircle: boolean}> = { |
||||||
|
1: { |
||||||
|
type: EFlightAreaType.DFENCE, |
||||||
|
isCircle: true, |
||||||
|
}, |
||||||
|
2: { |
||||||
|
type: EFlightAreaType.DFENCE, |
||||||
|
isCircle: false, |
||||||
|
}, |
||||||
|
3: { |
||||||
|
type: EFlightAreaType.NFZ, |
||||||
|
isCircle: true, |
||||||
|
}, |
||||||
|
4: { |
||||||
|
type: EFlightAreaType.NFZ, |
||||||
|
isCircle: false, |
||||||
|
}, |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
const selectedKey = ref<string>('1') |
||||||
|
const selectAction = (item: any) => { |
||||||
|
selectedKey.value = item.key |
||||||
|
emit('select-action', actionMap[item.key]) |
||||||
|
} |
||||||
|
const selectCurrent = () => { |
||||||
|
emit('click', actionMap[selectedKey.value]) |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss"> |
||||||
|
.icon-panel { |
||||||
|
align-items: center; |
||||||
|
justify-content: center; |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,197 @@ |
|||||||
|
<template> |
||||||
|
<div class="flight-area-device-panel"> |
||||||
|
<Title title="Choose Synchronous Devices"> |
||||||
|
<div style="position: absolute; right: 10px;"> |
||||||
|
<a style="color: white;" @click="closePanel"><CloseOutlined /></a> |
||||||
|
</div> |
||||||
|
</Title> |
||||||
|
<div class="scrollbar"> |
||||||
|
<div id="data" v-if="data.length !== 0"> |
||||||
|
<div v-for="dock in data" :key="dock.device_sn"> |
||||||
|
<div class="pt5 panel flex-row" @click="selectDock(dock)" :style="{opacity: selectedDocksMap[dock.device_sn] ? 1 : 0.5 }"> |
||||||
|
<div style="width: 88%"> |
||||||
|
<div class="title"> |
||||||
|
<RobotFilled class="fz20"/> |
||||||
|
<a-tooltip :title="dock.nickname"> |
||||||
|
<div class="pr10 ml5" style="width: 120px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">{{ dock.nickname }}</div> |
||||||
|
</a-tooltip> |
||||||
|
</div> |
||||||
|
<div class="ml10 mr10 pr5 pl5 flex-align-center flex-row flex-justify-between" style="background: #595959;"> |
||||||
|
<div> |
||||||
|
Custom Flight Area |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<div v-if="!dock.status"> |
||||||
|
<a-tooltip title="Dock offline"> |
||||||
|
<ApiOutlined /> |
||||||
|
</a-tooltip> |
||||||
|
</div> |
||||||
|
<div v-else-if="deviceStatusMap[dock.device_sn]?.flight_area_status?.sync_status === ESyncStatus.SYNCHRONIZED"> |
||||||
|
<a-tooltip title="Data synced"> |
||||||
|
<CheckCircleTwoTone twoToneColor="#28d445"/> |
||||||
|
</a-tooltip> |
||||||
|
</div> |
||||||
|
<div v-else-if="deviceStatusMap[dock.device_sn]?.flight_area_status?.sync_status === ESyncStatus.SYNCHRONIZING |
||||||
|
|| deviceStatusMap[dock.device_sn]?.flight_area_status?.sync_status === ESyncStatus.WAIT_SYNC"> |
||||||
|
<a-tooltip title="To be synced"> |
||||||
|
<SyncOutlined spin /> |
||||||
|
</a-tooltip> |
||||||
|
</div> |
||||||
|
<div v-else> |
||||||
|
<a-tooltip :title="deviceStatusMap[dock.device_sn]?.flight_area_status?.sync_msg || 'No synchronization'"> |
||||||
|
<ExclamationCircleTwoTone twoToneColor="#e70102" /> |
||||||
|
</a-tooltip> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="box" v-if="selectedDocksMap[dock.device_sn]"> |
||||||
|
<CheckOutlined /> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<DividerLine style="position: absolute; bottom: 68px;" /> |
||||||
|
<div class="flex-row flex-justify-between footer"> |
||||||
|
<a-button class="mr10" @click="closePanel">Cancel |
||||||
|
</a-button> |
||||||
|
<a-button type="primary" :disabled="confirmDisabled" @click="syncDeviceFlightArea">Sync |
||||||
|
</a-button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div v-else> |
||||||
|
<a-empty :image-style="{ height: '60px', marginTop: '60px' }" /> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
</template> |
||||||
|
|
||||||
|
<script lang="ts" setup> |
||||||
|
import { CloseOutlined, RobotFilled, CheckOutlined, ApiOutlined, CheckCircleTwoTone, SyncOutlined, ExclamationCircleTwoTone } from '@ant-design/icons-vue' |
||||||
|
import Title from '/@/components/workspace/Title.vue' |
||||||
|
import { defineEmits, onMounted, ref, defineProps, computed } from 'vue' |
||||||
|
import { getBindingDevices } from '/@/api/manage' |
||||||
|
import { EDeviceTypeName, ELocalStorageKey } from '/@/types' |
||||||
|
import { IPage } from '/@/api/http/type' |
||||||
|
import { Device } from '/@/types/device' |
||||||
|
import DividerLine from '../workspace/DividerLine.vue' |
||||||
|
import { message } from 'ant-design-vue' |
||||||
|
import { GetDeviceStatus, syncFlightArea } from '/@/api/flight-area' |
||||||
|
import { ESyncStatus } from '/@/types/flight-area' |
||||||
|
|
||||||
|
const props = defineProps<{ |
||||||
|
data: GetDeviceStatus[] |
||||||
|
}>() |
||||||
|
const emit = defineEmits(['closePanel']) |
||||||
|
const closePanel = () => { |
||||||
|
emit('closePanel', false) |
||||||
|
} |
||||||
|
|
||||||
|
const confirmDisabled = ref(false) |
||||||
|
|
||||||
|
const deviceStatusMap = computed(() => props.data.reduce((obj: Record<string, GetDeviceStatus>, val: GetDeviceStatus) => { |
||||||
|
obj[val.device_sn] = val |
||||||
|
return obj |
||||||
|
}, {} as Record<string, GetDeviceStatus>)) |
||||||
|
const workspaceId = localStorage.getItem(ELocalStorageKey.WorkspaceId) || '' |
||||||
|
const body: IPage = { |
||||||
|
page: 1, |
||||||
|
total: 0, |
||||||
|
page_size: 10, |
||||||
|
} |
||||||
|
const data = ref<Device[]>([]) |
||||||
|
const selectedDocksMap = ref<Record<string, boolean>>({}) |
||||||
|
|
||||||
|
const getDocks = async () => { |
||||||
|
await getBindingDevices(workspaceId, body, EDeviceTypeName.Dock).then(res => { |
||||||
|
if (res.code !== 0) { |
||||||
|
return |
||||||
|
} |
||||||
|
data.value.push(...res.data.list) |
||||||
|
body.page = res.data.pagination.page |
||||||
|
body.page_size = res.data.pagination.page_size |
||||||
|
body.total = res.data.pagination.total |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const selectDock = (dock: Device) => { |
||||||
|
if (!dock.status) { |
||||||
|
message.info(`Dock(${dock.nickname}) is offline.`) |
||||||
|
return |
||||||
|
} |
||||||
|
if (deviceStatusMap.value[dock.device_sn]?.flight_area_status?.sync_status === ESyncStatus.SYNCHRONIZING || |
||||||
|
deviceStatusMap.value[dock.device_sn]?.flight_area_status?.sync_status === ESyncStatus.WAIT_SYNC) { |
||||||
|
message.info('The dock is synchronizing.') |
||||||
|
return |
||||||
|
} |
||||||
|
selectedDocksMap.value[dock.device_sn] = !selectedDocksMap.value[dock.device_sn] |
||||||
|
} |
||||||
|
|
||||||
|
onMounted(() => { |
||||||
|
getDocks() |
||||||
|
const key = setInterval(() => { |
||||||
|
if (body.total === 0 || Math.ceil(body.total / body.page_size) <= body.page) { |
||||||
|
clearInterval(key) |
||||||
|
return |
||||||
|
} |
||||||
|
body.page++ |
||||||
|
getDocks() |
||||||
|
}, 1000) |
||||||
|
}) |
||||||
|
|
||||||
|
const syncDeviceFlightArea = () => { |
||||||
|
const keys = Object.keys(selectedDocksMap.value) |
||||||
|
if (keys.length === 0) { |
||||||
|
message.warn('Please select the docks that need to be synchronized.') |
||||||
|
return |
||||||
|
} |
||||||
|
confirmDisabled.value = true |
||||||
|
Object.keys(selectedDocksMap.value).forEach(k => { |
||||||
|
const device = deviceStatusMap.value[k] |
||||||
|
if (device) { |
||||||
|
device.flight_area_status = { sync_code: 0, sync_status: ESyncStatus.WAIT_SYNC, sync_msg: '' } |
||||||
|
} |
||||||
|
}) |
||||||
|
syncFlightArea(keys).then(res => { |
||||||
|
if (res.code === 0) { |
||||||
|
message.success('The devices are synchronizing...') |
||||||
|
selectedDocksMap.value = {} |
||||||
|
} |
||||||
|
}).finally(() => setTimeout(() => { |
||||||
|
confirmDisabled.value = false |
||||||
|
}, 3000)) |
||||||
|
} |
||||||
|
|
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.flight-area-device-panel { |
||||||
|
position: absolute; |
||||||
|
left: 285px; |
||||||
|
width: 280px; |
||||||
|
height: 100vh; |
||||||
|
float: right; |
||||||
|
top: 0; |
||||||
|
z-index: 1000; |
||||||
|
color: white; |
||||||
|
background: #282828; |
||||||
|
.footer { |
||||||
|
position: absolute; |
||||||
|
width: 100%; |
||||||
|
bottom: 10px; |
||||||
|
padding: 10px; |
||||||
|
button { |
||||||
|
width: 45%; |
||||||
|
border: 0; |
||||||
|
} |
||||||
|
} |
||||||
|
.scrollbar { |
||||||
|
overflow-y: auto; |
||||||
|
height: calc(100vh - 150px); |
||||||
|
} |
||||||
|
.box { |
||||||
|
font-size: 22px; |
||||||
|
line-height: 60px; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,33 @@ |
|||||||
|
<template> |
||||||
|
<div class="flex-row flex-align-center"> |
||||||
|
<div class="shape" :class="type" :style="isCircle ? 'border-radius: 50%;' : ''"></div> |
||||||
|
<div class="ml5" v-if="!hideTitle">{{ FlightAreaTypeTitleMap[type][isCircle ? EGeometryType.CIRCLE : EGeometryType.POLYGON] }}</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script lang="ts" setup> |
||||||
|
import { defineProps } from 'vue' |
||||||
|
import { EFlightAreaType, EGeometryType, FlightAreaTypeTitleMap } from '../../types/flight-area' |
||||||
|
|
||||||
|
const props = defineProps<{ |
||||||
|
type: EFlightAreaType, |
||||||
|
isCircle: boolean, |
||||||
|
hideTitle?: boolean |
||||||
|
}>() |
||||||
|
|
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss"> |
||||||
|
.nfz { |
||||||
|
border-color: red; |
||||||
|
} |
||||||
|
.dfence { |
||||||
|
border-color: $tag-green; |
||||||
|
} |
||||||
|
.shape { |
||||||
|
width: 16px; |
||||||
|
height: 16px; |
||||||
|
border-width: 3px; |
||||||
|
border-style: solid; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,89 @@ |
|||||||
|
<template> |
||||||
|
<div class="panel" style="padding-top: 5px;" :class="{disable: !flightArea.status}"> |
||||||
|
<div class="title"> |
||||||
|
<a-tooltip :title="flightArea.name"> |
||||||
|
<div class="pr10" style="white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">{{ flightArea.name }}</div> |
||||||
|
</a-tooltip> |
||||||
|
</div> |
||||||
|
<div class="mt5 ml10" style="color: hsla(0,0%,100%,0.35);"> |
||||||
|
<span class="mr10">Update at {{ formatDateTime(flightArea.update_time).toLocaleString() }}</span> |
||||||
|
</div> |
||||||
|
<div class="flex-row flex-justify-between flex-align-center ml10 mt5" style="color: hsla(0,0%,100%,0.65);"> |
||||||
|
<FlightAreaIcon :type="flightArea.type" :isCircle="EGeometryType.CIRCLE === flightArea.content.geometry.type"/> |
||||||
|
<div class="mr10 operate"> |
||||||
|
<a-popconfirm v-if="flightArea.status" title="Is it determined to disable the current area?" okText="Disable" @confirm="changeAreaStatus(false)"> |
||||||
|
<stop-outlined /> |
||||||
|
</a-popconfirm> |
||||||
|
<a-popconfirm v-else @confirm="changeAreaStatus(true)" title="Is it determined to enable the current area?" okText="Enable" > |
||||||
|
<check-circle-outlined /> |
||||||
|
</a-popconfirm> |
||||||
|
<EnvironmentFilled class="ml10" @click="clickLocation"/> |
||||||
|
<a-popconfirm title="Is it determined to delete the current area?" okText="Delete" okType="danger" @confirm="deleteArea"> |
||||||
|
<delete-outlined class="ml10" /> |
||||||
|
</a-popconfirm> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script lang="ts" setup> |
||||||
|
import { defineProps, reactive, defineEmits, computed } from 'vue' |
||||||
|
import { GetFlightArea, changeFlightAreaStatus } from '../../api/flight-area' |
||||||
|
import FlightAreaIcon from './FlightAreaIcon.vue' |
||||||
|
import { formatDateTime } from '../../utils/time' |
||||||
|
import { EGeometryType } from '../../types/flight-area' |
||||||
|
import { StopOutlined, CheckCircleOutlined, DeleteOutlined, EnvironmentFilled } from '@ant-design/icons-vue' |
||||||
|
|
||||||
|
const props = defineProps<{ |
||||||
|
data: GetFlightArea |
||||||
|
}>() |
||||||
|
const emit = defineEmits(['delete', 'update', 'location']) |
||||||
|
|
||||||
|
const flightArea = computed(() => props.data) |
||||||
|
const changeAreaStatus = (status: boolean) => { |
||||||
|
changeFlightAreaStatus(props.data.area_id, status).then(res => { |
||||||
|
if (res.code === 0) { |
||||||
|
flightArea.value.status = status |
||||||
|
emit('update', flightArea) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
const deleteArea = () => { |
||||||
|
emit('delete', flightArea.value.area_id) |
||||||
|
} |
||||||
|
const clickLocation = () => { |
||||||
|
emit('location', flightArea.value.area_id) |
||||||
|
} |
||||||
|
|
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.panel { |
||||||
|
background: #3c3c3c; |
||||||
|
margin-left: auto; |
||||||
|
margin-right: auto; |
||||||
|
margin-top: 10px; |
||||||
|
height: 90px; |
||||||
|
width: 95%; |
||||||
|
font-size: 13px; |
||||||
|
border-radius: 2px; |
||||||
|
cursor: pointer; |
||||||
|
|
||||||
|
.title { |
||||||
|
display: flex; |
||||||
|
flex-direction: row; |
||||||
|
align-items: center; |
||||||
|
height: 30px; |
||||||
|
font-weight: bold; |
||||||
|
margin: 0px 10px 0 10px; |
||||||
|
} |
||||||
|
.operate > *{ |
||||||
|
font-size: 16px; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.disable { |
||||||
|
opacity: 50%; |
||||||
|
} |
||||||
|
|
||||||
|
</style> |
@ -0,0 +1,43 @@ |
|||||||
|
<template> |
||||||
|
<div class="flight-area-panel"> |
||||||
|
<div v-if="data.length === 0"> |
||||||
|
<a-empty :image-style="{ height: '60px', marginTop: '60px' }" /> |
||||||
|
</div> |
||||||
|
<div v-else v-for="area in flightAreaList" :key="area.area_id"> |
||||||
|
<FlightAreaItem :data="area" @delete="deleteArea" @update="updateArea" @location="clickLocation(area)"/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script lang="ts" setup> |
||||||
|
import { defineProps, defineEmits, ref, computed } from 'vue' |
||||||
|
import FlightAreaItem from './FlightAreaItem.vue' |
||||||
|
import { GetFlightArea } from '/@/api/flight-area' |
||||||
|
|
||||||
|
const emit = defineEmits(['deleteArea', 'updateArea', 'locationArea']) |
||||||
|
|
||||||
|
const props = defineProps<{ |
||||||
|
data: GetFlightArea[] |
||||||
|
}>() |
||||||
|
|
||||||
|
const flightAreaList = computed(() => props.data) |
||||||
|
|
||||||
|
const deleteArea = (areaId: string) => { |
||||||
|
emit('deleteArea', areaId) |
||||||
|
} |
||||||
|
|
||||||
|
const updateArea = (area: GetFlightArea) => { |
||||||
|
emit('updateArea', area) |
||||||
|
} |
||||||
|
|
||||||
|
const clickLocation = (area: GetFlightArea) => { |
||||||
|
emit('locationArea', area) |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.flight-area-panel { |
||||||
|
overflow-y: auto; |
||||||
|
height: calc(100vh - 150px); |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,66 @@ |
|||||||
|
<template> |
||||||
|
<div class="flight-area-sync-panel p10 flex-row flex-align-center" > |
||||||
|
<RobotFilled class="fz30" twoToneColor="red" fill="#00ff00"/> |
||||||
|
<div class="ml20 mr10 flex-column" @click="switchPanel"> |
||||||
|
<div class="fz18">Sync Across Devices</div> |
||||||
|
<div v-if="syncDevicesCount > 0"><a-spin /> Syncing to {{ syncDevicesCount }} devices</div> |
||||||
|
</div> |
||||||
|
<RightOutlined class="fz18" @click="switchPanel"/> |
||||||
|
<FlightAreaDevicePanel v-if="visible" @close-panel="closePanel" :data="syncDevices"/> |
||||||
|
</div> |
||||||
|
|
||||||
|
</template> |
||||||
|
|
||||||
|
<script lang="ts" setup> |
||||||
|
import { RobotFilled, RightOutlined } from '@ant-design/icons-vue' |
||||||
|
import FlightAreaDevicePanel from '/@/components/flight-area/FlightAreaDevicePanel.vue' |
||||||
|
import { computed, onMounted, ref, watch } from 'vue' |
||||||
|
import { GetDeviceStatus, getDeviceStatus } from '/@/api/flight-area' |
||||||
|
import { ESyncStatus, FlightAreaSyncProgress } from '/@/types/flight-area' |
||||||
|
import { useFlightAreaSyncProgressEvent } from './use-flight-area-sync-progress-event' |
||||||
|
|
||||||
|
const visible = ref(false) |
||||||
|
const syncDevices = ref<GetDeviceStatus[]>([]) |
||||||
|
const syncDevicesCount = computed(() => syncDevices.value.filter(device => |
||||||
|
device.flight_area_status.sync_status === ESyncStatus.SYNCHRONIZING || device.flight_area_status.sync_status === ESyncStatus.WAIT_SYNC).length) |
||||||
|
const getAllDeviceStatus = () => { |
||||||
|
getDeviceStatus().then(res => { |
||||||
|
if (res.code === 0) { |
||||||
|
syncDevices.value = res.data |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
onMounted(() => { |
||||||
|
getAllDeviceStatus() |
||||||
|
}) |
||||||
|
const switchPanel = () => { |
||||||
|
visible.value = !visible.value |
||||||
|
} |
||||||
|
const closePanel = (val: boolean) => { |
||||||
|
visible.value = val |
||||||
|
} |
||||||
|
|
||||||
|
const handleSyncProgress = (data: FlightAreaSyncProgress) => { |
||||||
|
let has = false |
||||||
|
const status = { sync_code: data.result, sync_status: data.status, sync_msg: data.message } |
||||||
|
syncDevices.value.forEach(device => { |
||||||
|
if (data.sn === device.device_sn) { |
||||||
|
device.flight_area_status = status |
||||||
|
has = true |
||||||
|
} |
||||||
|
}) |
||||||
|
if (!has) { |
||||||
|
syncDevices.value.push({ device_sn: data.sn, flight_area_status: status }) |
||||||
|
} |
||||||
|
} |
||||||
|
useFlightAreaSyncProgressEvent(handleSyncProgress) |
||||||
|
|
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.flight-area-sync-panel { |
||||||
|
height: 70px; |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,18 @@ |
|||||||
|
import { FlightAreasDroneLocation } from '/@/types/flight-area' |
||||||
|
import { CommonHostWs } from '/@/websocket' |
||||||
|
import EventBus from '/@/event-bus/' |
||||||
|
import { onMounted, onBeforeUnmount } from 'vue' |
||||||
|
|
||||||
|
export function useFlightAreaDroneLocationEvent (onFlightAreaDroneLocationWs: (data: CommonHostWs<FlightAreasDroneLocation>) => void): void { |
||||||
|
function handleDroneLocationEvent (data: any) { |
||||||
|
onFlightAreaDroneLocationWs(data.data) |
||||||
|
} |
||||||
|
|
||||||
|
onMounted(() => { |
||||||
|
EventBus.on('flightAreasDroneLocationWs', handleDroneLocationEvent) |
||||||
|
}) |
||||||
|
|
||||||
|
onBeforeUnmount(() => { |
||||||
|
EventBus.off('flightAreasDroneLocationWs', handleDroneLocationEvent) |
||||||
|
}) |
||||||
|
} |
@ -0,0 +1,17 @@ |
|||||||
|
import EventBus from '/@/event-bus/' |
||||||
|
import { onMounted, onBeforeUnmount } from 'vue' |
||||||
|
import { FlightAreaSyncProgress } from '/@/types/flight-area' |
||||||
|
|
||||||
|
export function useFlightAreaSyncProgressEvent (onFlightAreaSyncProgressWs: (data: FlightAreaSyncProgress) => void): void { |
||||||
|
function handleSyncProgressEvent (data: FlightAreaSyncProgress) { |
||||||
|
onFlightAreaSyncProgressWs(data) |
||||||
|
} |
||||||
|
|
||||||
|
onMounted(() => { |
||||||
|
EventBus.on('flightAreasSyncProgressWs', handleSyncProgressEvent) |
||||||
|
}) |
||||||
|
|
||||||
|
onBeforeUnmount(() => { |
||||||
|
EventBus.off('flightAreasSyncProgressWs', handleSyncProgressEvent) |
||||||
|
}) |
||||||
|
} |
@ -0,0 +1,30 @@ |
|||||||
|
import { EFlightAreaUpdate, FlightAreaUpdate, FlightAreasDroneLocation } from '/@/types/flight-area' |
||||||
|
import { CommonHostWs } from '/@/websocket' |
||||||
|
import EventBus from '/@/event-bus/' |
||||||
|
import { onMounted, onBeforeUnmount } from 'vue' |
||||||
|
|
||||||
|
function doNothing (data: FlightAreaUpdate) { |
||||||
|
} |
||||||
|
export function useFlightAreaUpdateEvent (addFunc = doNothing, deleteFunc = doNothing, updateFunc = doNothing): void { |
||||||
|
function handleDroneLocationEvent (data: FlightAreaUpdate) { |
||||||
|
switch (data.operation) { |
||||||
|
case EFlightAreaUpdate.ADD: |
||||||
|
addFunc(data) |
||||||
|
break |
||||||
|
case EFlightAreaUpdate.UPDATE: |
||||||
|
updateFunc(data) |
||||||
|
break |
||||||
|
case EFlightAreaUpdate.DELETE: |
||||||
|
deleteFunc(data) |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
onMounted(() => { |
||||||
|
EventBus.on('flightAreasUpdateWs', handleDroneLocationEvent) |
||||||
|
}) |
||||||
|
|
||||||
|
onBeforeUnmount(() => { |
||||||
|
EventBus.off('flightAreasUpdateWs', handleDroneLocationEvent) |
||||||
|
}) |
||||||
|
} |
@ -0,0 +1,155 @@ |
|||||||
|
import { message, notification } from 'ant-design-vue' |
||||||
|
import { MapDoodleEnum } from '/@/types/map-enum' |
||||||
|
import { getRoot } from '/@/root' |
||||||
|
import { PostFlightAreaBody, saveFlightArea } from '/@/api/flight-area' |
||||||
|
import { generateCircleContent, generatePolyContent } from '/@/utils/map-layer-utils' |
||||||
|
import { GeojsonCoordinate } from '/@/utils/genjson' |
||||||
|
import { gcj02towgs84, wgs84togcj02 } from '/@/vendors/coordtransform.js' |
||||||
|
import { uuidv4 } from '/@/utils/uuid' |
||||||
|
import { CommonHostWs } from '/@/websocket' |
||||||
|
import { FlightAreasDroneLocation } from '/@/types/flight-area' |
||||||
|
import rootStore from '/@/store' |
||||||
|
import { h } from 'vue' |
||||||
|
import { useGMapCover } from '/@/hooks/use-g-map-cover' |
||||||
|
import moment from 'moment' |
||||||
|
import { DATE_FORMAT } from '/@/utils/constants' |
||||||
|
|
||||||
|
export function useFlightArea () { |
||||||
|
const root = getRoot() |
||||||
|
const store = rootStore |
||||||
|
const coverMap = store.state.coverMap |
||||||
|
|
||||||
|
let useGMapCoverHook = useGMapCover() |
||||||
|
|
||||||
|
const MIN_RADIUS = 10 |
||||||
|
function checkCircle (obj: any): boolean { |
||||||
|
if (obj.getRadius() < MIN_RADIUS) { |
||||||
|
message.error(`The radius must be greater than ${MIN_RADIUS}m.`) |
||||||
|
root.$map.remove(obj) |
||||||
|
return false |
||||||
|
} |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
function checkPolygon (obj: any): boolean { |
||||||
|
const path: any[][] = obj.getPath() |
||||||
|
if (path.length < 3) { |
||||||
|
message.error('The path of the polygon cannot be crossed.') |
||||||
|
root.$map.remove(obj) |
||||||
|
return false |
||||||
|
} |
||||||
|
// root.$aMap.GeometryUtil.doesLineLineIntersect()
|
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
function setExtData (obj: any) { |
||||||
|
let ext = obj.getExtData() |
||||||
|
const id = uuidv4() |
||||||
|
const name = `${ext.type}-${moment().format(DATE_FORMAT)}` |
||||||
|
ext = Object.assign({}, ext, { id, name }) |
||||||
|
obj.setExtData(ext) |
||||||
|
return ext |
||||||
|
} |
||||||
|
function createFlightArea (obj: any) { |
||||||
|
const ext = obj.getExtData() |
||||||
|
const data = { |
||||||
|
id: ext.id, |
||||||
|
type: ext.type, |
||||||
|
name: ext.name, |
||||||
|
} |
||||||
|
let coordinates: GeojsonCoordinate | GeojsonCoordinate[][] |
||||||
|
let content |
||||||
|
switch (ext.mapType) { |
||||||
|
case 'circle': |
||||||
|
content = generateCircleContent(obj.getCenter(), obj.getRadius()) |
||||||
|
coordinates = getWgs84(content.geometry.coordinates as GeojsonCoordinate) |
||||||
|
break |
||||||
|
case 'polygon': |
||||||
|
content = generatePolyContent(obj.getPath()).content |
||||||
|
coordinates = [getWgs84(content.geometry.coordinates[0] as GeojsonCoordinate[])] |
||||||
|
break |
||||||
|
default: |
||||||
|
message.error(`Invalid type: ${obj.mapType}`) |
||||||
|
root.$map.remove(obj) |
||||||
|
return |
||||||
|
} |
||||||
|
content.geometry.coordinates = coordinates |
||||||
|
|
||||||
|
saveFlightArea(Object.assign({}, data, { content }) as PostFlightAreaBody).then(res => { |
||||||
|
if (res.code !== 0) { |
||||||
|
useGMapCoverHook.removeCoverFromMap(ext.id) |
||||||
|
} |
||||||
|
}).finally(() => root.$map.remove(obj)) |
||||||
|
} |
||||||
|
|
||||||
|
function getDrawFlightAreaCallback (obj: any) { |
||||||
|
useGMapCoverHook = useGMapCover() |
||||||
|
const ext = setExtData(obj) |
||||||
|
switch (ext.mapType) { |
||||||
|
case MapDoodleEnum.CIRCLE: |
||||||
|
if (!checkCircle(obj)) { |
||||||
|
return |
||||||
|
} |
||||||
|
break |
||||||
|
case MapDoodleEnum.POLYGON: |
||||||
|
if (!checkPolygon(obj)) { |
||||||
|
return |
||||||
|
} |
||||||
|
break |
||||||
|
default: |
||||||
|
break |
||||||
|
} |
||||||
|
createFlightArea(obj) |
||||||
|
} |
||||||
|
|
||||||
|
const getWgs84 = <T extends GeojsonCoordinate | GeojsonCoordinate[]>(coordinate: T): T => { |
||||||
|
if (coordinate[0] instanceof Array) { |
||||||
|
return (coordinate as GeojsonCoordinate[]).map(c => gcj02towgs84(c[0], c[1])) as T |
||||||
|
} |
||||||
|
return gcj02towgs84(coordinate[0], coordinate[1]) |
||||||
|
} |
||||||
|
|
||||||
|
const getGcj02 = <T extends GeojsonCoordinate | GeojsonCoordinate[]>(coordinate: T): T => { |
||||||
|
if (coordinate[0] instanceof Array) { |
||||||
|
return (coordinate as GeojsonCoordinate[]).map(c => wgs84togcj02(c[0], c[1])) as T |
||||||
|
} |
||||||
|
return wgs84togcj02(coordinate[0], coordinate[1]) |
||||||
|
} |
||||||
|
|
||||||
|
const onFlightAreaDroneLocationWs = (data: CommonHostWs<FlightAreasDroneLocation>) => { |
||||||
|
const nearArea = data.host.drone_locations.filter(val => !val.is_in_area) |
||||||
|
const inArea = data.host.drone_locations.filter(val => val.is_in_area) |
||||||
|
notification.warning({ |
||||||
|
key: `flight-area-${data.sn}`, |
||||||
|
message: `Drone(${data.sn}) flight area information`, |
||||||
|
description: h('div', |
||||||
|
[ |
||||||
|
h('div', [ |
||||||
|
h('span', { class: 'fz18' }, 'In the flight area: '), |
||||||
|
h('ul', [ |
||||||
|
...inArea.map(val => h('li', `There are ${val.area_distance} meters from the edge of the area(${coverMap[val.area_id][1]?.getText() || val.area_id}).`)) |
||||||
|
]) |
||||||
|
]), |
||||||
|
h('div', [ |
||||||
|
h('span', { class: 'fz18' }, 'Near the flight area: '), |
||||||
|
h('ul', [ |
||||||
|
...nearArea.map(val => h('li', `There are ${val.area_distance} meters from the edge of the area(${coverMap[val.area_id][1]?.getText() || val.area_id}).`)) |
||||||
|
]) |
||||||
|
]) |
||||||
|
]), |
||||||
|
duration: null, |
||||||
|
style: { |
||||||
|
width: '420px', |
||||||
|
marginTop: '-8px', |
||||||
|
marginLeft: '-28px', |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
getDrawFlightAreaCallback, |
||||||
|
getGcj02, |
||||||
|
getWgs84, |
||||||
|
onFlightAreaDroneLocationWs, |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,15 @@ |
|||||||
|
<template> |
||||||
|
<Divider class="divider" /> |
||||||
|
</template> |
||||||
|
<script lang="ts" setup> |
||||||
|
import { Divider } from 'ant-design-vue' |
||||||
|
|
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.divider { |
||||||
|
margin: 10px 0; |
||||||
|
height: 1px; |
||||||
|
background-color: #4f4f4f; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,21 @@ |
|||||||
|
<template> |
||||||
|
<div style="height: 40px; line-height: 50px; font-weight: 450;"> |
||||||
|
<a-row> |
||||||
|
<a-col :span="1"></a-col> |
||||||
|
<a-col :span="(23 - (extSpan || 0))">{{ title }}</a-col> |
||||||
|
<a-col :span="extSpan"><slot /></a-col> |
||||||
|
</a-row> |
||||||
|
</div> |
||||||
|
<DividerLine /> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script lang="ts" setup> |
||||||
|
import { defineProps } from 'vue' |
||||||
|
import DividerLine from '/@/components/workspace/DividerLine.vue' |
||||||
|
|
||||||
|
const props = defineProps < { |
||||||
|
extSpan?: number, |
||||||
|
title: string, |
||||||
|
} >() |
||||||
|
|
||||||
|
</script> |
@ -0,0 +1,16 @@ |
|||||||
|
import { GeojsonCoordinate } from '../utils/genjson' |
||||||
|
import { getRoot } from '/@/root' |
||||||
|
|
||||||
|
export function useMapTool () { |
||||||
|
const root = getRoot() |
||||||
|
const map = root.$map |
||||||
|
const AMap = root.$aMap |
||||||
|
|
||||||
|
function panTo (coordinate: GeojsonCoordinate) { |
||||||
|
map.panTo(coordinate, 100) |
||||||
|
map.setZoom(18, false, 100) |
||||||
|
} |
||||||
|
return { |
||||||
|
panTo, |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,98 @@ |
|||||||
|
<template> |
||||||
|
<div class="project-flight-area-wrapper height-100"> |
||||||
|
<a-spin :spinning="loading" :delay="300" tip="loading" size="large" class="height-100"> |
||||||
|
<Title title="Custom Flight Area" /> |
||||||
|
<FlightAreaPanel :data="flightAreaList" @location-area="clickArea" @delete-area="deleteAreaById"/> |
||||||
|
<DividerLine /> |
||||||
|
<FlightAreaSyncPanel /> |
||||||
|
</a-spin> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script lang="ts" setup> |
||||||
|
import { onMounted, ref } from 'vue' |
||||||
|
import Title from '/@/components/workspace/Title.vue' |
||||||
|
import DividerLine from '/@/components/workspace/DividerLine.vue' |
||||||
|
import FlightAreaPanel from '/@/components/flight-area/FlightAreaPanel.vue' |
||||||
|
import FlightAreaSyncPanel from '/@/components/flight-area/FlightAreaSyncPanel.vue' |
||||||
|
import { GetFlightArea, deleteFlightArea, getFlightAreaList } from '/@/api/flight-area' |
||||||
|
import { useGMapCover } from '/@/hooks/use-g-map-cover' |
||||||
|
import { useMapTool } from '/@/hooks/use-map-tool' |
||||||
|
import { EFlightAreaType, EGeometryType, FlightAreaUpdate } from '/@/types/flight-area' |
||||||
|
import { useFlightArea } from '/@/components/flight-area/use-flight-area' |
||||||
|
import { useFlightAreaUpdateEvent } from '/@/components/flight-area/use-flight-area-update' |
||||||
|
|
||||||
|
const loading = ref(false) |
||||||
|
const flightAreaList = ref<GetFlightArea[]>([]) |
||||||
|
let useGMapCoverHook = useGMapCover() |
||||||
|
let useMapToolHook = useMapTool() |
||||||
|
|
||||||
|
onMounted(() => { |
||||||
|
getDataList() |
||||||
|
}) |
||||||
|
const { getGcj02 } = useFlightArea() |
||||||
|
|
||||||
|
const initMapFlightArea = () => { |
||||||
|
useMapToolHook = useMapTool() |
||||||
|
useGMapCoverHook = useGMapCover() |
||||||
|
flightAreaList.value.forEach(area => { |
||||||
|
updateMapFlightArea(area) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const updateMapFlightArea = (area: GetFlightArea) => { |
||||||
|
switch (area.content.geometry.type) { |
||||||
|
case EGeometryType.CIRCLE: |
||||||
|
useGMapCoverHook.updateFlightAreaCircle(area.area_id, area.name, area.content.geometry.radius, getGcj02(area.content.geometry.coordinates), area.status, area.type) |
||||||
|
break |
||||||
|
case 'Polygon': |
||||||
|
useGMapCoverHook.updateFlightAreaPolygon(area.area_id, area.name, getGcj02(area.content.geometry.coordinates[0]), area.status, area.type) |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const getDataList = () => { |
||||||
|
loading.value = true |
||||||
|
getFlightAreaList().then(res => { |
||||||
|
flightAreaList.value = res.data |
||||||
|
setTimeout(initMapFlightArea, 2000) |
||||||
|
}).finally(() => { |
||||||
|
loading.value = false |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const deleteAreaById = (areaId: string) => { |
||||||
|
deleteFlightArea(areaId) |
||||||
|
} |
||||||
|
|
||||||
|
const deleteArea = (area: FlightAreaUpdate) => { |
||||||
|
flightAreaList.value = flightAreaList.value.filter(data => data.area_id !== area.area_id) |
||||||
|
useGMapCoverHook.removeCoverFromMap(area.area_id) |
||||||
|
} |
||||||
|
|
||||||
|
const updateArea = (area: FlightAreaUpdate) => { |
||||||
|
flightAreaList.value = flightAreaList.value.map(data => data.area_id === area.area_id ? area : data) |
||||||
|
updateMapFlightArea(area as GetFlightArea) |
||||||
|
} |
||||||
|
|
||||||
|
const addArea = (area: FlightAreaUpdate) => { |
||||||
|
flightAreaList.value.push(area as GetFlightArea) |
||||||
|
updateMapFlightArea(area as GetFlightArea) |
||||||
|
} |
||||||
|
|
||||||
|
useFlightAreaUpdateEvent(addArea, deleteArea, updateArea) |
||||||
|
|
||||||
|
const clickArea = (area: GetFlightArea) => { |
||||||
|
console.info(area) |
||||||
|
let coordinate |
||||||
|
switch (area.content.geometry.type) { |
||||||
|
case EGeometryType.CIRCLE: |
||||||
|
coordinate = getGcj02(area.content.geometry.coordinates) |
||||||
|
break |
||||||
|
case 'Polygon': |
||||||
|
coordinate = useGMapCoverHook.calcPolygonPosition(getGcj02(area.content.geometry.coordinates[0])) |
||||||
|
break |
||||||
|
} |
||||||
|
useMapToolHook.panTo(coordinate) |
||||||
|
} |
||||||
|
</script> |
@ -0,0 +1,80 @@ |
|||||||
|
import { GeojsonCoordinate, GeojsonPolygon } from '../utils/genjson' |
||||||
|
|
||||||
|
export enum EFlightAreaType { |
||||||
|
NFZ = 'nfz', |
||||||
|
DFENCE = 'dfence', |
||||||
|
} |
||||||
|
|
||||||
|
export enum EGeometryType { |
||||||
|
CIRCLE = 'Circle', |
||||||
|
POLYGON = 'Polygon', |
||||||
|
} |
||||||
|
|
||||||
|
export enum EFlightAreaUpdate { |
||||||
|
ADD = 'add', |
||||||
|
UPDATE = 'update', |
||||||
|
DELETE = 'delete', |
||||||
|
} |
||||||
|
|
||||||
|
export enum ESyncStatus { |
||||||
|
WAIT_SYNC = 'wait_sync', |
||||||
|
SWITCH_FAIL = 'switch_fail', |
||||||
|
SYNCHRONIZING = 'synchronizing', |
||||||
|
SYNCHRONIZED = 'synchronized', |
||||||
|
FAIL = 'fail', |
||||||
|
} |
||||||
|
|
||||||
|
export interface GeojsonCircle { |
||||||
|
type: 'Feature' |
||||||
|
properties: { |
||||||
|
color: string |
||||||
|
clampToGround?: boolean |
||||||
|
} |
||||||
|
geometry: { |
||||||
|
type: EGeometryType.CIRCLE |
||||||
|
coordinates: GeojsonCoordinate |
||||||
|
radius: number |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export interface DroneLocation { |
||||||
|
area_distance: number, |
||||||
|
area_id: string, |
||||||
|
is_in_area: boolean, |
||||||
|
} |
||||||
|
|
||||||
|
export interface FlightAreasDroneLocation { |
||||||
|
drone_locations: DroneLocation[] |
||||||
|
} |
||||||
|
|
||||||
|
export type FlightAreaContent = GeojsonCircle | GeojsonPolygon |
||||||
|
|
||||||
|
export interface FlightAreaUpdate { |
||||||
|
operation: EFlightAreaUpdate, |
||||||
|
area_id: string, |
||||||
|
name: string, |
||||||
|
type: EFlightAreaType, |
||||||
|
content: FlightAreaContent, |
||||||
|
status: boolean, |
||||||
|
username: string, |
||||||
|
create_time: number, |
||||||
|
update_time: number, |
||||||
|
} |
||||||
|
|
||||||
|
export interface FlightAreaSyncProgress { |
||||||
|
sn: string, |
||||||
|
result: number, |
||||||
|
status: ESyncStatus, |
||||||
|
message: string, |
||||||
|
} |
||||||
|
|
||||||
|
export const FlightAreaTypeTitleMap = { |
||||||
|
[EFlightAreaType.NFZ]: { |
||||||
|
[EGeometryType.CIRCLE]: 'Circular GEO Zone', |
||||||
|
[EGeometryType.POLYGON]: 'Polygonal GEO Zone', |
||||||
|
}, |
||||||
|
[EFlightAreaType.DFENCE]: { |
||||||
|
[EGeometryType.CIRCLE]: 'Circular Task Area', |
||||||
|
[EGeometryType.POLYGON]: 'Polygonal Task Area', |
||||||
|
}, |
||||||
|
} |
File diff suppressed because one or more lines are too long
@ -0,0 +1,687 @@ |
|||||||
|
|
||||||
|
//
|
||||||
|
// Copyright (c) 2013-2021 Winlin
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
|
||||||
|
'use strict'; |
||||||
|
|
||||||
|
function SrsError(name, message) { |
||||||
|
this.name = name; |
||||||
|
this.message = message; |
||||||
|
this.stack = (new Error()).stack; |
||||||
|
} |
||||||
|
SrsError.prototype = Object.create(Error.prototype); |
||||||
|
SrsError.prototype.constructor = SrsError; |
||||||
|
|
||||||
|
// Depends on adapter-7.4.0.min.js from https://github.com/webrtc/adapter
|
||||||
|
// Async-awat-prmise based SRS RTC Publisher.
|
||||||
|
function SrsRtcPublisherAsync() { |
||||||
|
var self = {}; |
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
|
||||||
|
self.constraints = { |
||||||
|
audio: true, |
||||||
|
video: { |
||||||
|
width: { ideal: 320, max: 576 } |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
// @see https://github.com/rtcdn/rtcdn-draft
|
||||||
|
// @url The WebRTC url to play with, for example:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream
|
||||||
|
// or specifies the API port:
|
||||||
|
// webrtc://r.ossrs.net:11985/live/livestream
|
||||||
|
// or autostart the publish:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?autostart=true
|
||||||
|
// or change the app from live to myapp:
|
||||||
|
// webrtc://r.ossrs.net:11985/myapp/livestream
|
||||||
|
// or change the stream from livestream to mystream:
|
||||||
|
// webrtc://r.ossrs.net:11985/live/mystream
|
||||||
|
// or set the api server to myapi.domain.com:
|
||||||
|
// webrtc://myapi.domain.com/live/livestream
|
||||||
|
// or set the candidate(eip) of answer:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?candidate=39.107.238.185
|
||||||
|
// or force to access https API:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?schema=https
|
||||||
|
// or use plaintext, without SRTP:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?encrypt=false
|
||||||
|
// or any other information, will pass-by in the query:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?vhost=xxx
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?token=xxx
|
||||||
|
self.publish = async function (url) { |
||||||
|
var conf = self.__internal.prepareUrl(url); |
||||||
|
self.pc.addTransceiver("audio", { direction: "sendonly" }); |
||||||
|
self.pc.addTransceiver("video", { direction: "sendonly" }); |
||||||
|
//self.pc.addTransceiver("video", {direction: "sendonly"});
|
||||||
|
//self.pc.addTransceiver("audio", {direction: "sendonly"});
|
||||||
|
|
||||||
|
if (!navigator.mediaDevices && window.location.protocol === 'http:' && window.location.hostname !== 'localhost') { |
||||||
|
throw new SrsError('HttpsRequiredError', `Please use HTTPS or localhost to publish, read https://github.com/ossrs/srs/issues/2762#issuecomment-983147576`); |
||||||
|
} |
||||||
|
var stream = await navigator.mediaDevices.getUserMedia(self.constraints); |
||||||
|
|
||||||
|
// @see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addStream#Migrating_to_addTrack
|
||||||
|
stream.getTracks().forEach(function (track) { |
||||||
|
self.pc.addTrack(track); |
||||||
|
|
||||||
|
// Notify about local track when stream is ok.
|
||||||
|
self.ontrack && self.ontrack({ track: track }); |
||||||
|
}); |
||||||
|
|
||||||
|
var offer = await self.pc.createOffer(); |
||||||
|
await self.pc.setLocalDescription(offer); |
||||||
|
var session = await new Promise(function (resolve, reject) { |
||||||
|
// @see https://github.com/rtcdn/rtcdn-draft
|
||||||
|
var data = { |
||||||
|
api: conf.apiUrl, tid: conf.tid, streamurl: conf.streamUrl, |
||||||
|
clientip: null, sdp: offer.sdp |
||||||
|
}; |
||||||
|
console.log("Generated offer: ", data); |
||||||
|
|
||||||
|
const xhr = new XMLHttpRequest(); |
||||||
|
xhr.onload = function () { |
||||||
|
if (xhr.readyState !== xhr.DONE) return; |
||||||
|
if (xhr.status !== 200 && xhr.status !== 201) return reject(xhr); |
||||||
|
const data = JSON.parse(xhr.responseText); |
||||||
|
console.log("Got answer: ", data); |
||||||
|
return data.code ? reject(xhr) : resolve(data); |
||||||
|
} |
||||||
|
xhr.open('POST', conf.apiUrl, true); |
||||||
|
xhr.setRequestHeader('Content-type', 'application/json'); |
||||||
|
xhr.send(JSON.stringify(data)); |
||||||
|
}); |
||||||
|
await self.pc.setRemoteDescription( |
||||||
|
new RTCSessionDescription({ type: 'answer', sdp: session.sdp }) |
||||||
|
); |
||||||
|
session.simulator = conf.schema + '//' + conf.urlObject.server + ':' + conf.port + '/rtc/v1/nack/'; |
||||||
|
|
||||||
|
return session; |
||||||
|
}; |
||||||
|
|
||||||
|
// Close the publisher.
|
||||||
|
self.close = function () { |
||||||
|
self.pc && self.pc.close(); |
||||||
|
self.pc = null; |
||||||
|
}; |
||||||
|
|
||||||
|
// The callback when got local stream.
|
||||||
|
// @see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addStream#Migrating_to_addTrack
|
||||||
|
self.ontrack = function (event) { |
||||||
|
// Add track to stream of SDK.
|
||||||
|
self.stream.addTrack(event.track); |
||||||
|
}; |
||||||
|
|
||||||
|
// Internal APIs.
|
||||||
|
self.__internal = { |
||||||
|
defaultPath: '/rtc/v1/publish/', |
||||||
|
prepareUrl: function (webrtcUrl) { |
||||||
|
var urlObject = self.__internal.parse(webrtcUrl); |
||||||
|
|
||||||
|
// If user specifies the schema, use it as API schema.
|
||||||
|
var schema = urlObject.user_query.schema; |
||||||
|
schema = schema ? schema + ':' : window.location.protocol; |
||||||
|
|
||||||
|
var port = urlObject.port || 1985; |
||||||
|
if (schema === 'https:') { |
||||||
|
port = urlObject.port || 443; |
||||||
|
} |
||||||
|
|
||||||
|
// @see https://github.com/rtcdn/rtcdn-draft
|
||||||
|
var api = urlObject.user_query.play || self.__internal.defaultPath; |
||||||
|
if (api.lastIndexOf('/') !== api.length - 1) { |
||||||
|
api += '/'; |
||||||
|
} |
||||||
|
|
||||||
|
var apiUrl = schema + '//' + urlObject.server + ':' + port + api; |
||||||
|
for (var key in urlObject.user_query) { |
||||||
|
if (key !== 'api' && key !== 'play') { |
||||||
|
apiUrl += '&' + key + '=' + urlObject.user_query[key]; |
||||||
|
} |
||||||
|
} |
||||||
|
// Replace /rtc/v1/play/&k=v to /rtc/v1/play/?k=v
|
||||||
|
apiUrl = apiUrl.replace(api + '&', api + '?'); |
||||||
|
|
||||||
|
var streamUrl = urlObject.url; |
||||||
|
|
||||||
|
return { |
||||||
|
apiUrl: apiUrl, streamUrl: streamUrl, schema: schema, urlObject: urlObject, port: port, |
||||||
|
tid: Number(parseInt(new Date().getTime() * Math.random() * 100)).toString(16).slice(0, 7) |
||||||
|
}; |
||||||
|
}, |
||||||
|
parse: function (url) { |
||||||
|
// @see: http://stackoverflow.com/questions/10469575/how-to-use-location-object-to-parse-url-without-redirecting-the-page-in-javascri
|
||||||
|
var a = document.createElement("a"); |
||||||
|
a.href = url.replace("rtmp://", "http://") |
||||||
|
.replace("webrtc://", "http://") |
||||||
|
.replace("rtc://", "http://"); |
||||||
|
|
||||||
|
var vhost = a.hostname; |
||||||
|
var app = a.pathname.substring(1, a.pathname.lastIndexOf("/")); |
||||||
|
var stream = a.pathname.slice(a.pathname.lastIndexOf("/") + 1); |
||||||
|
|
||||||
|
// parse the vhost in the params of app, that srs supports.
|
||||||
|
app = app.replace("...vhost...", "?vhost="); |
||||||
|
if (app.indexOf("?") >= 0) { |
||||||
|
var params = app.slice(app.indexOf("?")); |
||||||
|
app = app.slice(0, app.indexOf("?")); |
||||||
|
|
||||||
|
if (params.indexOf("vhost=") > 0) { |
||||||
|
vhost = params.slice(params.indexOf("vhost=") + "vhost=".length); |
||||||
|
if (vhost.indexOf("&") > 0) { |
||||||
|
vhost = vhost.slice(0, vhost.indexOf("&")); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// when vhost equals to server, and server is ip,
|
||||||
|
// the vhost is __defaultVhost__
|
||||||
|
if (a.hostname === vhost) { |
||||||
|
var re = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/; |
||||||
|
if (re.test(a.hostname)) { |
||||||
|
vhost = "__defaultVhost__"; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// parse the schema
|
||||||
|
var schema = "rtmp"; |
||||||
|
if (url.indexOf("://") > 0) { |
||||||
|
schema = url.slice(0, url.indexOf("://")); |
||||||
|
} |
||||||
|
|
||||||
|
var port = a.port; |
||||||
|
if (!port) { |
||||||
|
// Finger out by webrtc url, if contains http or https port, to overwrite default 1985.
|
||||||
|
if (schema === 'webrtc' && url.indexOf(`webrtc://${a.host}:`) === 0) { |
||||||
|
port = (url.indexOf(`webrtc://${a.host}:80`) === 0) ? 80 : 443; |
||||||
|
} |
||||||
|
|
||||||
|
// Guess by schema.
|
||||||
|
if (schema === 'http') { |
||||||
|
port = 80; |
||||||
|
} else if (schema === 'https') { |
||||||
|
port = 443; |
||||||
|
} else if (schema === 'rtmp') { |
||||||
|
port = 1935; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var ret = { |
||||||
|
url: url, |
||||||
|
schema: schema, |
||||||
|
server: a.hostname, port: port, |
||||||
|
vhost: vhost, app: app, stream: stream |
||||||
|
}; |
||||||
|
self.__internal.fill_query(a.search, ret); |
||||||
|
|
||||||
|
// For webrtc API, we use 443 if page is https, or schema specified it.
|
||||||
|
if (!ret.port) { |
||||||
|
if (schema === 'webrtc' || schema === 'rtc') { |
||||||
|
if (ret.user_query.schema === 'https') { |
||||||
|
ret.port = 443; |
||||||
|
} else if (window.location.href.indexOf('https://') === 0) { |
||||||
|
ret.port = 443; |
||||||
|
} else { |
||||||
|
// For WebRTC, SRS use 1985 as default API port.
|
||||||
|
ret.port = 1985; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return ret; |
||||||
|
}, |
||||||
|
fill_query: function (query_string, obj) { |
||||||
|
// pure user query object.
|
||||||
|
obj.user_query = {}; |
||||||
|
|
||||||
|
if (query_string.length === 0) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// split again for angularjs.
|
||||||
|
if (query_string.indexOf("?") >= 0) { |
||||||
|
query_string = query_string.split("?")[1]; |
||||||
|
} |
||||||
|
|
||||||
|
var queries = query_string.split("&"); |
||||||
|
for (var i = 0; i < queries.length; i++) { |
||||||
|
var elem = queries[i]; |
||||||
|
|
||||||
|
var query = elem.split("="); |
||||||
|
obj[query[0]] = query[1]; |
||||||
|
obj.user_query[query[0]] = query[1]; |
||||||
|
} |
||||||
|
|
||||||
|
// alias domain for vhost.
|
||||||
|
if (obj.domain) { |
||||||
|
obj.vhost = obj.domain; |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
self.pc = new RTCPeerConnection(null); |
||||||
|
|
||||||
|
// To keep api consistent between player and publisher.
|
||||||
|
// @see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addStream#Migrating_to_addTrack
|
||||||
|
// @see https://webrtc.org/getting-started/media-devices
|
||||||
|
self.stream = new MediaStream(); |
||||||
|
|
||||||
|
return self; |
||||||
|
} |
||||||
|
|
||||||
|
// Depends on adapter-7.4.0.min.js from https://github.com/webrtc/adapter
|
||||||
|
// Async-await-promise based SRS RTC Player.
|
||||||
|
function SrsRtcPlayerAsync() { |
||||||
|
var self = {}; |
||||||
|
|
||||||
|
// @see https://github.com/rtcdn/rtcdn-draft
|
||||||
|
// @url The WebRTC url to play with, for example:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream
|
||||||
|
// or specifies the API port:
|
||||||
|
// webrtc://r.ossrs.net:11985/live/livestream
|
||||||
|
// webrtc://r.ossrs.net:80/live/livestream
|
||||||
|
// or autostart the play:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?autostart=true
|
||||||
|
// or change the app from live to myapp:
|
||||||
|
// webrtc://r.ossrs.net:11985/myapp/livestream
|
||||||
|
// or change the stream from livestream to mystream:
|
||||||
|
// webrtc://r.ossrs.net:11985/live/mystream
|
||||||
|
// or set the api server to myapi.domain.com:
|
||||||
|
// webrtc://myapi.domain.com/live/livestream
|
||||||
|
// or set the candidate(eip) of answer:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?candidate=39.107.238.185
|
||||||
|
// or force to access https API:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?schema=https
|
||||||
|
// or use plaintext, without SRTP:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?encrypt=false
|
||||||
|
// or any other information, will pass-by in the query:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?vhost=xxx
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?token=xxx
|
||||||
|
self.play = async function (url) { |
||||||
|
var conf = self.__internal.prepareUrl(url); |
||||||
|
self.pc.addTransceiver("audio", { direction: "recvonly" }); |
||||||
|
self.pc.addTransceiver("video", { direction: "recvonly" }); |
||||||
|
//self.pc.addTransceiver("video", {direction: "recvonly"});
|
||||||
|
//self.pc.addTransceiver("audio", {direction: "recvonly"});
|
||||||
|
|
||||||
|
var offer = await self.pc.createOffer(); |
||||||
|
await self.pc.setLocalDescription(offer); |
||||||
|
var session = await new Promise(function (resolve, reject) { |
||||||
|
// @see https://github.com/rtcdn/rtcdn-draft
|
||||||
|
var data = { |
||||||
|
api: conf.apiUrl, tid: conf.tid, streamurl: conf.streamUrl, |
||||||
|
clientip: null, sdp: offer.sdp |
||||||
|
}; |
||||||
|
console.log("Generated offer: ", data); |
||||||
|
|
||||||
|
const xhr = new XMLHttpRequest(); |
||||||
|
xhr.onload = function () { |
||||||
|
if (xhr.readyState !== xhr.DONE) return; |
||||||
|
if (xhr.status !== 200 && xhr.status !== 201) return reject(xhr); |
||||||
|
const data = JSON.parse(xhr.responseText); |
||||||
|
console.log("Got answer: ", data); |
||||||
|
return data.code ? reject(xhr) : resolve(data); |
||||||
|
} |
||||||
|
xhr.open('POST', conf.apiUrl, true); |
||||||
|
xhr.setRequestHeader('Content-type', 'application/json'); |
||||||
|
xhr.send(JSON.stringify(data)); |
||||||
|
}); |
||||||
|
await self.pc.setRemoteDescription( |
||||||
|
new RTCSessionDescription({ type: 'answer', sdp: session.sdp }) |
||||||
|
); |
||||||
|
session.simulator = conf.schema + '//' + conf.urlObject.server + ':' + conf.port + '/rtc/v1/nack/'; |
||||||
|
|
||||||
|
return session; |
||||||
|
}; |
||||||
|
|
||||||
|
// Close the player.
|
||||||
|
self.close = function () { |
||||||
|
self.pc && self.pc.close(); |
||||||
|
self.pc = null; |
||||||
|
}; |
||||||
|
|
||||||
|
// The callback when got remote track.
|
||||||
|
// Note that the onaddstream is deprecated, @see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/onaddstream
|
||||||
|
self.ontrack = function (event) { |
||||||
|
// https://webrtc.org/getting-started/remote-streams
|
||||||
|
self.stream.addTrack(event.track); |
||||||
|
}; |
||||||
|
|
||||||
|
// Internal APIs.
|
||||||
|
self.__internal = { |
||||||
|
defaultPath: '/rtc/v1/play/', |
||||||
|
prepareUrl: function (webrtcUrl) { |
||||||
|
var urlObject = self.__internal.parse(webrtcUrl); |
||||||
|
|
||||||
|
// If user specifies the schema, use it as API schema.
|
||||||
|
var schema = urlObject.user_query.schema; |
||||||
|
schema = schema ? schema + ':' : window.location.protocol; |
||||||
|
|
||||||
|
var port = urlObject.port || 1985; |
||||||
|
if (schema === 'https:') { |
||||||
|
port = urlObject.port || 443; |
||||||
|
} |
||||||
|
|
||||||
|
// @see https://github.com/rtcdn/rtcdn-draft
|
||||||
|
var api = urlObject.user_query.play || self.__internal.defaultPath; |
||||||
|
if (api.lastIndexOf('/') !== api.length - 1) { |
||||||
|
api += '/'; |
||||||
|
} |
||||||
|
|
||||||
|
var apiUrl = schema + '//' + urlObject.server + ':' + port + api; |
||||||
|
for (var key in urlObject.user_query) { |
||||||
|
if (key !== 'api' && key !== 'play') { |
||||||
|
apiUrl += '&' + key + '=' + urlObject.user_query[key]; |
||||||
|
} |
||||||
|
} |
||||||
|
// Replace /rtc/v1/play/&k=v to /rtc/v1/play/?k=v
|
||||||
|
apiUrl = apiUrl.replace(api + '&', api + '?'); |
||||||
|
|
||||||
|
var streamUrl = urlObject.url; |
||||||
|
|
||||||
|
return { |
||||||
|
apiUrl: apiUrl, streamUrl: streamUrl, schema: schema, urlObject: urlObject, port: port, |
||||||
|
tid: Number(parseInt(new Date().getTime() * Math.random() * 100)).toString(16).slice(0, 7) |
||||||
|
}; |
||||||
|
}, |
||||||
|
parse: function (url) { |
||||||
|
// @see: http://stackoverflow.com/questions/10469575/how-to-use-location-object-to-parse-url-without-redirecting-the-page-in-javascri
|
||||||
|
var a = document.createElement("a"); |
||||||
|
a.href = url.replace("rtmp://", "http://") |
||||||
|
.replace("webrtc://", "http://") |
||||||
|
.replace("rtc://", "http://"); |
||||||
|
|
||||||
|
var vhost = a.hostname; |
||||||
|
var app = a.pathname.substring(1, a.pathname.lastIndexOf("/")); |
||||||
|
var stream = a.pathname.slice(a.pathname.lastIndexOf("/") + 1); |
||||||
|
|
||||||
|
// parse the vhost in the params of app, that srs supports.
|
||||||
|
app = app.replace("...vhost...", "?vhost="); |
||||||
|
if (app.indexOf("?") >= 0) { |
||||||
|
var params = app.slice(app.indexOf("?")); |
||||||
|
app = app.slice(0, app.indexOf("?")); |
||||||
|
|
||||||
|
if (params.indexOf("vhost=") > 0) { |
||||||
|
vhost = params.slice(params.indexOf("vhost=") + "vhost=".length); |
||||||
|
if (vhost.indexOf("&") > 0) { |
||||||
|
vhost = vhost.slice(0, vhost.indexOf("&")); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// when vhost equals to server, and server is ip,
|
||||||
|
// the vhost is __defaultVhost__
|
||||||
|
if (a.hostname === vhost) { |
||||||
|
var re = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/; |
||||||
|
if (re.test(a.hostname)) { |
||||||
|
vhost = "__defaultVhost__"; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// parse the schema
|
||||||
|
var schema = "rtmp"; |
||||||
|
if (url.indexOf("://") > 0) { |
||||||
|
schema = url.slice(0, url.indexOf("://")); |
||||||
|
} |
||||||
|
|
||||||
|
var port = a.port; |
||||||
|
if (!port) { |
||||||
|
// Finger out by webrtc url, if contains http or https port, to overwrite default 1985.
|
||||||
|
if (schema === 'webrtc' && url.indexOf(`webrtc://${a.host}:`) === 0) { |
||||||
|
port = (url.indexOf(`webrtc://${a.host}:80`) === 0) ? 80 : 443; |
||||||
|
} |
||||||
|
|
||||||
|
// Guess by schema.
|
||||||
|
if (schema === 'http') { |
||||||
|
port = 80; |
||||||
|
} else if (schema === 'https') { |
||||||
|
port = 443; |
||||||
|
} else if (schema === 'rtmp') { |
||||||
|
port = 1935; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var ret = { |
||||||
|
url: url, |
||||||
|
schema: schema, |
||||||
|
server: a.hostname, port: port, |
||||||
|
vhost: vhost, app: app, stream: stream |
||||||
|
}; |
||||||
|
self.__internal.fill_query(a.search, ret); |
||||||
|
|
||||||
|
// For webrtc API, we use 443 if page is https, or schema specified it.
|
||||||
|
if (!ret.port) { |
||||||
|
if (schema === 'webrtc' || schema === 'rtc') { |
||||||
|
if (ret.user_query.schema === 'https') { |
||||||
|
ret.port = 443; |
||||||
|
} else if (window.location.href.indexOf('https://') === 0) { |
||||||
|
ret.port = 443; |
||||||
|
} else { |
||||||
|
// For WebRTC, SRS use 1985 as default API port.
|
||||||
|
ret.port = 1985; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return ret; |
||||||
|
}, |
||||||
|
fill_query: function (query_string, obj) { |
||||||
|
// pure user query object.
|
||||||
|
obj.user_query = {}; |
||||||
|
|
||||||
|
if (query_string.length === 0) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// split again for angularjs.
|
||||||
|
if (query_string.indexOf("?") >= 0) { |
||||||
|
query_string = query_string.split("?")[1]; |
||||||
|
} |
||||||
|
|
||||||
|
var queries = query_string.split("&"); |
||||||
|
for (var i = 0; i < queries.length; i++) { |
||||||
|
var elem = queries[i]; |
||||||
|
|
||||||
|
var query = elem.split("="); |
||||||
|
obj[query[0]] = query[1]; |
||||||
|
obj.user_query[query[0]] = query[1]; |
||||||
|
} |
||||||
|
|
||||||
|
// alias domain for vhost.
|
||||||
|
if (obj.domain) { |
||||||
|
obj.vhost = obj.domain; |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
self.pc = new RTCPeerConnection(null); |
||||||
|
|
||||||
|
// Create a stream to add track to the stream, @see https://webrtc.org/getting-started/remote-streams
|
||||||
|
self.stream = new MediaStream(); |
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/ontrack
|
||||||
|
self.pc.ontrack = function (event) { |
||||||
|
if (self.ontrack) { |
||||||
|
self.ontrack(event); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
return self; |
||||||
|
} |
||||||
|
|
||||||
|
// Depends on adapter-7.4.0.min.js from https://github.com/webrtc/adapter
|
||||||
|
// Async-awat-prmise based SRS RTC Publisher by WHIP.
|
||||||
|
function SrsRtcWhipWhepAsync() { |
||||||
|
var self = {}; |
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
|
||||||
|
self.constraints = { |
||||||
|
audio: true, |
||||||
|
video: { |
||||||
|
width: { ideal: 320, max: 576 } |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
// See https://datatracker.ietf.org/doc/draft-ietf-wish-whip/
|
||||||
|
// @url The WebRTC url to publish with, for example:
|
||||||
|
// http://localhost:1985/rtc/v1/whip/?app=live&stream=livestream
|
||||||
|
self.publish = async function (url) { |
||||||
|
if (url.indexOf('/whip/') === -1) throw new Error(`invalid WHIP url ${url}`); |
||||||
|
|
||||||
|
self.pc.addTransceiver("audio", { direction: "sendonly" }); |
||||||
|
self.pc.addTransceiver("video", { direction: "sendonly" }); |
||||||
|
|
||||||
|
if (!navigator.mediaDevices && window.location.protocol === 'http:' && window.location.hostname !== 'localhost') { |
||||||
|
throw new SrsError('HttpsRequiredError', `Please use HTTPS or localhost to publish, read https://github.com/ossrs/srs/issues/2762#issuecomment-983147576`); |
||||||
|
} |
||||||
|
var stream = await navigator.mediaDevices.getUserMedia(self.constraints); |
||||||
|
|
||||||
|
// @see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addStream#Migrating_to_addTrack
|
||||||
|
stream.getTracks().forEach(function (track) { |
||||||
|
self.pc.addTrack(track); |
||||||
|
|
||||||
|
// Notify about local track when stream is ok.
|
||||||
|
self.ontrack && self.ontrack({ track: track }); |
||||||
|
}); |
||||||
|
|
||||||
|
var offer = await self.pc.createOffer(); |
||||||
|
await self.pc.setLocalDescription(offer); |
||||||
|
const answer = await new Promise(function (resolve, reject) { |
||||||
|
console.log("Generated offer: ", offer); |
||||||
|
|
||||||
|
const xhr = new XMLHttpRequest(); |
||||||
|
xhr.onload = function () { |
||||||
|
if (xhr.readyState !== xhr.DONE) return; |
||||||
|
if (xhr.status !== 200 && xhr.status !== 201) return reject(xhr); |
||||||
|
const data = xhr.responseText; |
||||||
|
console.log("Got answer: ", data); |
||||||
|
return data.code ? reject(xhr) : resolve(data); |
||||||
|
} |
||||||
|
xhr.open('POST', url, true); |
||||||
|
xhr.setRequestHeader('Content-type', 'application/sdp'); |
||||||
|
xhr.send(offer.sdp); |
||||||
|
}); |
||||||
|
await self.pc.setRemoteDescription( |
||||||
|
new RTCSessionDescription({ type: 'answer', sdp: answer }) |
||||||
|
); |
||||||
|
|
||||||
|
return self.__internal.parseId(url, offer.sdp, answer); |
||||||
|
}; |
||||||
|
|
||||||
|
// See https://datatracker.ietf.org/doc/draft-ietf-wish-whip/
|
||||||
|
// @url The WebRTC url to play with, for example:
|
||||||
|
// http://localhost:1985/rtc/v1/whep/?app=live&stream=livestream
|
||||||
|
self.play = async function (url) { |
||||||
|
if (url.indexOf('/whip-play/') === -1 && url.indexOf('/whep/') === -1) throw new Error(`invalid WHEP url ${url}`); |
||||||
|
|
||||||
|
self.pc.addTransceiver("video", { direction: "recvonly" }); |
||||||
|
|
||||||
|
var offer = await self.pc.createOffer(); |
||||||
|
await self.pc.setLocalDescription(offer); |
||||||
|
const answer = await new Promise(function (resolve, reject) { |
||||||
|
console.log("Generated offer: ", offer); |
||||||
|
|
||||||
|
const xhr = new XMLHttpRequest(); |
||||||
|
xhr.onload = function () { |
||||||
|
if (xhr.readyState !== xhr.DONE) return; |
||||||
|
if (xhr.status !== 200 && xhr.status !== 201) return reject(xhr); |
||||||
|
const data = xhr.responseText; |
||||||
|
console.log("Got answer: ", data); |
||||||
|
return data.code ? reject(xhr) : resolve(data); |
||||||
|
} |
||||||
|
xhr.open('POST', url, true); |
||||||
|
xhr.setRequestHeader('Content-type', 'application/sdp'); |
||||||
|
xhr.send(offer.sdp); |
||||||
|
}); |
||||||
|
await self.pc.setRemoteDescription( |
||||||
|
new RTCSessionDescription({ type: 'answer', sdp: answer }) |
||||||
|
); |
||||||
|
|
||||||
|
return self.__internal.parseId(url, offer.sdp, answer); |
||||||
|
}; |
||||||
|
|
||||||
|
// Close the publisher.
|
||||||
|
self.close = function () { |
||||||
|
self.pc && self.pc.close(); |
||||||
|
self.pc = null; |
||||||
|
}; |
||||||
|
|
||||||
|
// The callback when got local stream.
|
||||||
|
// @see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addStream#Migrating_to_addTrack
|
||||||
|
self.ontrack = function (event) { |
||||||
|
// Add track to stream of SDK.
|
||||||
|
self.stream.addTrack(event.track); |
||||||
|
}; |
||||||
|
|
||||||
|
self.pc = new RTCPeerConnection(null); |
||||||
|
|
||||||
|
// To keep api consistent between player and publisher.
|
||||||
|
// @see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addStream#Migrating_to_addTrack
|
||||||
|
// @see https://webrtc.org/getting-started/media-devices
|
||||||
|
self.stream = new MediaStream(); |
||||||
|
|
||||||
|
// Internal APIs.
|
||||||
|
self.__internal = { |
||||||
|
parseId: (url, offer, answer) => { |
||||||
|
let sessionid = offer.substr(offer.indexOf('a=ice-ufrag:') + 'a=ice-ufrag:'.length); |
||||||
|
sessionid = sessionid.substr(0, sessionid.indexOf('\n') - 1) + ':'; |
||||||
|
sessionid += answer.substr(answer.indexOf('a=ice-ufrag:') + 'a=ice-ufrag:'.length); |
||||||
|
sessionid = sessionid.substr(0, sessionid.indexOf('\n')); |
||||||
|
|
||||||
|
const a = document.createElement("a"); |
||||||
|
a.href = url; |
||||||
|
return { |
||||||
|
sessionid: sessionid, // Should be ice-ufrag of answer:offer.
|
||||||
|
simulator: a.protocol + '//' + a.host + '/rtc/v1/nack/', |
||||||
|
}; |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/ontrack
|
||||||
|
self.pc.ontrack = function (event) { |
||||||
|
if (self.ontrack) { |
||||||
|
self.ontrack(event); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
return self; |
||||||
|
} |
||||||
|
|
||||||
|
// Format the codec of RTCRtpSender, kind(audio/video) is optional filter.
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/Media/Formats/WebRTC_codecs#getting_the_supported_codecs
|
||||||
|
function SrsRtcFormatSenders(senders, kind) { |
||||||
|
var codecs = []; |
||||||
|
senders.forEach(function (sender) { |
||||||
|
var params = sender.getParameters(); |
||||||
|
params && params.codecs && params.codecs.forEach(function (c) { |
||||||
|
if (kind && sender.track.kind !== kind) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (c.mimeType.indexOf('/red') > 0 || c.mimeType.indexOf('/rtx') > 0 || c.mimeType.indexOf('/fec') > 0) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
var s = ''; |
||||||
|
|
||||||
|
s += c.mimeType.replace('audio/', '').replace('video/', ''); |
||||||
|
s += ', ' + c.clockRate + 'HZ'; |
||||||
|
if (sender.track.kind === "audio") { |
||||||
|
s += ', channels: ' + c.channels; |
||||||
|
} |
||||||
|
s += ', pt: ' + c.payloadType; |
||||||
|
|
||||||
|
codecs.push(s); |
||||||
|
}); |
||||||
|
}); |
||||||
|
return codecs.join(", "); |
||||||
|
} |
||||||
|
|
||||||
|
export default { |
||||||
|
SrsError, |
||||||
|
SrsRtcPublisherAsync, |
||||||
|
SrsRtcPlayerAsync, |
||||||
|
SrsRtcWhipWhepAsync, |
||||||
|
SrsRtcFormatSenders, |
||||||
|
} |
Loading…
Reference in new issue