mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 13:33:42 +08:00
106 lines
7.3 KiB
JavaScript
106 lines
7.3 KiB
JavaScript
// Local recording inventory and closed camera playback.
|
|
export const LOCAL = /^(?:[a-f0-9]{8}--[a-f0-9]{10}|[0-9]{4}-[0-9]{2}-[0-9]{2}--[0-9]{2}-[0-9]{2}-[0-9]{2}|[a-f0-9]{16}\|(?:[a-f0-9]{8}--[a-f0-9]{10}|[0-9]{4}-[0-9]{2}-[0-9]{2}--[0-9]{2}-[0-9]{2}-[0-9]{2}))$/
|
|
export const SEGMENT = /^(?:[a-f0-9]{8}--[a-f0-9]{10}|[0-9]{4}-[0-9]{2}-[0-9]{2}--[0-9]{2}-[0-9]{2}-[0-9]{2}|[a-f0-9]{16}[|_](?:[a-f0-9]{8}--[a-f0-9]{10}|[0-9]{4}-[0-9]{2}-[0-9]{2}--[0-9]{2}-[0-9]{2}-[0-9]{2}))--[0-9]{1,6}$/
|
|
const FILES = ["rlog", "qlog", "fcamera", "dcamera", "ecamera", "qcamera"]
|
|
export const LABELS = { rlog: "Full log", qlog: "Quick log", fcamera: "Road video", dcamera: "Driver video",
|
|
ecamera: "Wide video", qcamera: "Quick video" }
|
|
|
|
export function validLocalHistory(value, complete = false) {
|
|
if (value?.schemaVersion !== 1 || value.source !== "local" || (!complete && value.partialHistory !== true) ||
|
|
typeof value.scanIncomplete !== "boolean" || !Array.isArray(value.routes) || value.routes.length > (complete ? 1 : 100)) return false
|
|
let count = 0
|
|
const routes = new Set()
|
|
return value.routes.every((route) => {
|
|
if (typeof route?.routeId !== "string" || !LOCAL.test(route.routeId) || routes.has(route.routeId) ||
|
|
!Array.isArray(route.segments) || route.segments.length > 512 ||
|
|
!Number.isSafeInteger(route.segmentCount) || route.segmentCount !== route.segments.length) return false
|
|
routes.add(route.routeId)
|
|
const numbers = new Set()
|
|
count += route.segmentCount
|
|
return count <= 512 && route.segments.every((segment) => {
|
|
if (!Number.isSafeInteger(segment?.number) || segment.number < 0 || segment.number > 999999 ||
|
|
typeof segment.segmentName !== "string" || !SEGMENT.test(segment.segmentName) ||
|
|
(!segment.segmentName.startsWith(route.routeId.replace('|', '_') + '--') &&
|
|
!segment.segmentName.startsWith(route.routeId + '--')) ||
|
|
Number(segment.segmentName.slice(segment.segmentName.lastIndexOf('--') + 2)) !== segment.number ||
|
|
numbers.has(segment.number) || segment.files === null || typeof segment.files !== "object" ||
|
|
Object.keys(segment.files).length !== FILES.length ||
|
|
!FILES.every((key) => typeof segment.files[key] === "boolean") ||
|
|
(!complete && !FILES.some((key) => segment.files[key]))) return false
|
|
numbers.add(segment.number)
|
|
return true
|
|
})
|
|
})
|
|
}
|
|
|
|
export const VIDEOS = ["fcamera", "dcamera", "ecamera", "qcamera"]
|
|
export const hasVideo = (files) => VIDEOS.some((key) => files?.[key])
|
|
export const availableFiles = (files) => FILES.filter((key) => files?.[key]).map((key) => LABELS[key])
|
|
export const routeFiles = (route) => availableFiles(Object.fromEntries(FILES.map((key) =>
|
|
[key, route.segments.some((segment) => segment.files[key])])))
|
|
export const firstQuickVideo = (route) => route.segments.find((segment) => segment.files.qcamera) || null
|
|
export function routeDate(route, locale = undefined, timeZone = undefined) {
|
|
const validTime = (value) => typeof value === "number" && Number.isFinite(value) && value > 0 && Number.isFinite(new Date(value * 1000).getTime())
|
|
const captured = validTime(route.startTime)
|
|
const value = captured ? route.startTime : validTime(route.fileTime) ? route.fileTime : null
|
|
if (value === null) return { label: "Date unavailable", source: "", datetime: null }
|
|
const date = new Date(value * 1000)
|
|
return { label: new Intl.DateTimeFormat(locale, { year: "numeric", month: "short", day: "numeric",
|
|
hour: "numeric", minute: "2-digit", ...(timeZone ? { timeZone } : {}) }).format(date),
|
|
source: captured ? "" : "File date", datetime: date.toISOString() }
|
|
}
|
|
export function connectRouteUrl(route) {
|
|
if (typeof route?.routeId !== "string" || !LOCAL.test(route.routeId) || typeof route.connectUrl !== "string") return null
|
|
const host = route.provider === "konik" ? "stable.konik.ai" : route.provider === undefined || route.provider === "comma" ? "connect.comma.ai" : null
|
|
if (host === null) return null
|
|
const match = /^https:\/\/(connect\.comma\.ai|stable\.konik\.ai)\/([a-f0-9]{16})\/([^/?#]+)$/.exec(route.connectUrl)
|
|
const [device, identifier] = route.routeId.includes('|') ? route.routeId.split('|') : [null, route.routeId]
|
|
return match && match[1] === host && match[3] === identifier && (device === null || match[2] === device) ? route.connectUrl : null
|
|
}
|
|
export const recordingUrl = (segmentName, camera = "qcamera") => SEGMENT.test(segmentName) &&
|
|
["qcamera", "fcamera", "dcamera", "ecamera"].includes(camera) ?
|
|
`./api/recordings/media/${encodeURIComponent(segmentName)}${camera === "qcamera" ? "" : "/" + camera}` : null
|
|
export const quickRoadUrl = (segmentName) => recordingUrl(segmentName)
|
|
export const segmentSummaryUrl = (segmentName) => SEGMENT.test(segmentName) ?
|
|
`./api/recordings/segment-summary?segmentName=${encodeURIComponent(segmentName)}` : null
|
|
|
|
export function validSegmentSummary(value, segmentName) {
|
|
const metric = (v) => v === null || (typeof v === "number" && Number.isFinite(v) && v >= 0)
|
|
return value?.schemaVersion === 1 && value.source === "closed_local_rlog" &&
|
|
value.segmentName === segmentName && typeof value.sourceSha256 === "string" &&
|
|
/^[a-f0-9]{64}$/.test(value.sourceSha256) &&
|
|
[value.observedCarSpanSeconds, value.estimatedDistanceMeters,
|
|
value.observedLatActiveSeconds, value.observedLongActiveSeconds].every(metric) &&
|
|
value.gaps && Number.isSafeInteger(value.gaps.carState) && value.gaps.carState >= 0 &&
|
|
Number.isSafeInteger(value.gaps.carControl) && value.gaps.carControl >= 0 &&
|
|
typeof value.sampleCoverageComplete === "boolean"
|
|
}
|
|
|
|
export const detailMetric = (seconds, unit = "s") => seconds === null ? "Unavailable" :
|
|
`${seconds.toFixed(unit === "m" ? 1 : 2)} ${unit}`
|
|
|
|
|
|
|
|
const searchText = value => String(value || "").normalize("NFKD").replace(/\p{M}/gu, "")
|
|
.toLocaleLowerCase().replace(/(\d+)(?:st|nd|rd|th)\b/g, "$1").replace(/[^\p{L}\p{N}]+/gu, " ").trim()
|
|
|
|
export function routeMatchesSearch(route, query) {
|
|
const date = routeDate(route)
|
|
const index = searchText(`${route.displayName || ""} ${route.routeId} ${date.label} ${date.datetime || ""}`)
|
|
return searchText(query).split(" ").every(word => index.includes(word))
|
|
}
|
|
|
|
export const driveVideoUrl = (routeId, camera) => LOCAL.test(routeId) && VIDEOS.includes(camera) ?
|
|
`./api/recordings/route/${encodeURIComponent(routeId)}/${camera}` : null
|
|
export const logUrl = (segmentName, filename) => SEGMENT.test(segmentName) && /^(rlog|qlog)\.(zst|bz2)$/.test(filename) ?
|
|
`./api/recordings/files/${encodeURIComponent(segmentName)}/${filename}` : null
|
|
export const logArchiveUrl = routeId => LOCAL.test(routeId) ? `./api/recordings/logs/${encodeURIComponent(routeId)}` : null
|
|
export const cameraLabel = camera => LABELS[camera]?.replace(" video", "")
|
|
export const formatDuration = minutes => minutes >= 60 ? `${Math.floor(minutes / 60)}h ${minutes % 60}m` : `${minutes} min`
|
|
export const formatBytes = bytes => bytes >= 1e9 ? `${(bytes / 1e9).toFixed(2)} GB` : `${((bytes || 0) / 1e6).toFixed(1)} MB`
|
|
export function validRouteDetail(value, routeId) {
|
|
return value?.schemaVersion === 1 && value.source === "local" && value.route?.routeId === routeId &&
|
|
typeof value.route.inventoryComplete === "boolean" && validLocalHistory({ schemaVersion: 1, source: "local",
|
|
scanIncomplete: !value.route.inventoryComplete, routes: [value.route] }, true)
|
|
}
|