Диагностика допуска правой камеры в интерфейс
This commit is contained in:
@@ -5,6 +5,8 @@ export type LiveViewerDiagnosticEventCode =
|
|||||||
| "live_receiver_recovery_exhausted"
|
| "live_receiver_recovery_exhausted"
|
||||||
| "live_receiver_active_store_admitted"
|
| "live_receiver_active_store_admitted"
|
||||||
| "live_receiver_error"
|
| "live_receiver_error"
|
||||||
|
| "live_camera_source_projected"
|
||||||
|
| "live_camera_window_admitted"
|
||||||
| "live_camera_transport_restart_requested"
|
| "live_camera_transport_restart_requested"
|
||||||
| "live_camera_transport_playing";
|
| "live_camera_transport_playing";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
import type { ObservationSourceDescriptor } from "../runtime/contracts";
|
import type { ObservationSourceDescriptor } from "../runtime/contracts";
|
||||||
|
import {
|
||||||
|
createLiveViewerInstanceId,
|
||||||
|
createLiveViewerLineage,
|
||||||
|
postLiveViewerDiagnostic,
|
||||||
|
type LiveViewerDiagnostic,
|
||||||
|
type LiveViewerLineage,
|
||||||
|
} from "./liveViewerDiagnostics";
|
||||||
import {
|
import {
|
||||||
closeObservationSource,
|
closeObservationSource,
|
||||||
openObservationSource,
|
openObservationSource,
|
||||||
@@ -88,6 +95,51 @@ export function automaticLivePresentationIdentity(
|
|||||||
return JSON.stringify([source.id, acquisitionId, deliveryId]);
|
return JSON.stringify([source.id, acquisitionId, deliveryId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LiveCameraPresentationDiagnostic {
|
||||||
|
key: string;
|
||||||
|
event: LiveViewerDiagnostic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read-only boundary markers for the automatic camera path. They distinguish
|
||||||
|
* a catalog/delivery failure from layout admission and player mounting without
|
||||||
|
* changing presentation state or contacting the device.
|
||||||
|
*/
|
||||||
|
export function pendingLiveCameraPresentationDiagnostics(
|
||||||
|
sources: readonly ObservationSourceDescriptor[],
|
||||||
|
visibleSourceIds: ReadonlySet<string>,
|
||||||
|
emittedKeys: ReadonlySet<string>,
|
||||||
|
): LiveCameraPresentationDiagnostic[] {
|
||||||
|
const pending: LiveCameraPresentationDiagnostic[] = [];
|
||||||
|
for (const source of sources) {
|
||||||
|
if (source.modality !== "video") continue;
|
||||||
|
const presentationIdentity = automaticLivePresentationIdentity(source);
|
||||||
|
const deliveryId = source.delivery?.id?.trim();
|
||||||
|
if (!presentationIdentity || !deliveryId) continue;
|
||||||
|
const projectedKey = `projected:${presentationIdentity}`;
|
||||||
|
if (!emittedKeys.has(projectedKey)) {
|
||||||
|
pending.push({
|
||||||
|
key: projectedKey,
|
||||||
|
event: {
|
||||||
|
eventCode: "live_camera_source_projected",
|
||||||
|
streamId: deliveryId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const admittedKey = `admitted:${presentationIdentity}`;
|
||||||
|
if (visibleSourceIds.has(source.id) && !emittedKeys.has(admittedKey)) {
|
||||||
|
pending.push({
|
||||||
|
key: admittedKey,
|
||||||
|
event: {
|
||||||
|
eventCode: "live_camera_window_admitted",
|
||||||
|
streamId: deliveryId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pending;
|
||||||
|
}
|
||||||
|
|
||||||
/** A deliberate close fences every delivery generation in that acquisition. */
|
/** A deliberate close fences every delivery generation in that acquisition. */
|
||||||
export function livePresentationCloseFence(
|
export function livePresentationCloseFence(
|
||||||
source: ObservationSourceDescriptor,
|
source: ObservationSourceDescriptor,
|
||||||
@@ -216,6 +268,8 @@ export function useObservationLayout(
|
|||||||
const admittedLivePresentationIdentitiesRef = useRef(new Set<string>());
|
const admittedLivePresentationIdentitiesRef = useRef(new Set<string>());
|
||||||
const closedLiveAcquisitionSourcesRef = useRef(new Set<string>());
|
const closedLiveAcquisitionSourcesRef = useRef(new Set<string>());
|
||||||
const presentedLiveAcquisitionSourcesRef = useRef(new Set<string>());
|
const presentedLiveAcquisitionSourcesRef = useRef(new Set<string>());
|
||||||
|
const emittedLiveCameraDiagnosticKeysRef = useRef(new Set<string>());
|
||||||
|
const liveCameraDiagnosticLineageRef = useRef<LiveViewerLineage | null>(null);
|
||||||
const initializedCatalog = useRef<string | null>(null);
|
const initializedCatalog = useRef<string | null>(null);
|
||||||
const sourceIdList = sources.map((source) => source.id).sort();
|
const sourceIdList = sources.map((source) => source.id).sort();
|
||||||
const sourceIdsIdentity = sourceIdList.join("\u0000");
|
const sourceIdsIdentity = sourceIdList.join("\u0000");
|
||||||
@@ -432,6 +486,23 @@ export function useObservationLayout(
|
|||||||
admitAutomaticLivePresentations();
|
admitAutomaticLivePresentations();
|
||||||
}, [admitAutomaticLivePresentations, selectedDeliveryIdentity]);
|
}, [admitAutomaticLivePresentations, selectedDeliveryIdentity]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const diagnostics = pendingLiveCameraPresentationDiagnostics(
|
||||||
|
sources,
|
||||||
|
new Set(visibleIds),
|
||||||
|
emittedLiveCameraDiagnosticKeysRef.current,
|
||||||
|
);
|
||||||
|
if (!diagnostics.length) return;
|
||||||
|
liveCameraDiagnosticLineageRef.current ??= createLiveViewerLineage(
|
||||||
|
createLiveViewerInstanceId(),
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
for (const diagnostic of diagnostics) {
|
||||||
|
emittedLiveCameraDiagnosticKeysRef.current.add(diagnostic.key);
|
||||||
|
postLiveViewerDiagnostic(diagnostic.event, liveCameraDiagnosticLineageRef.current);
|
||||||
|
}
|
||||||
|
}, [selectedDeliveryIdentity, sources, visibleIds]);
|
||||||
|
|
||||||
const markPending = useCallback((source: ObservationSourceDescriptor, pending: boolean) => {
|
const markPending = useCallback((source: ObservationSourceDescriptor, pending: boolean) => {
|
||||||
const groupId = source.activation?.groupId;
|
const groupId = source.activation?.groupId;
|
||||||
const affected = groupId
|
const affected = groupId
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ let WorkspaceLayoutApiError;
|
|||||||
let WorkspaceLayoutContractError;
|
let WorkspaceLayoutContractError;
|
||||||
let admitLiveDefaultPresentations;
|
let admitLiveDefaultPresentations;
|
||||||
let automaticLivePresentationIdentity;
|
let automaticLivePresentationIdentity;
|
||||||
|
let pendingLiveCameraPresentationDiagnostics;
|
||||||
let livePresentationCloseFence;
|
let livePresentationCloseFence;
|
||||||
let restoredLayoutMayAdmitLiveDefault;
|
let restoredLayoutMayAdmitLiveDefault;
|
||||||
let observationPresentationSourceAfterLayoutApply;
|
let observationPresentationSourceAfterLayoutApply;
|
||||||
@@ -39,6 +40,7 @@ before(async () => {
|
|||||||
({
|
({
|
||||||
admitLiveDefaultPresentations,
|
admitLiveDefaultPresentations,
|
||||||
automaticLivePresentationIdentity,
|
automaticLivePresentationIdentity,
|
||||||
|
pendingLiveCameraPresentationDiagnostics,
|
||||||
livePresentationCloseFence,
|
livePresentationCloseFence,
|
||||||
restoredLayoutMayAdmitLiveDefault,
|
restoredLayoutMayAdmitLiveDefault,
|
||||||
observationPresentationSourceAfterLayoutApply,
|
observationPresentationSourceAfterLayoutApply,
|
||||||
@@ -375,6 +377,68 @@ test("a restored layout admits same-lineage replacements and the next acquisitio
|
|||||||
assert.deepEqual(deliberatelyClosed.admittedIdentities, []);
|
assert.deepEqual(deliberatelyClosed.admittedIdentities, []);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("live camera diagnostics distinguish catalog projection from window admission", () => {
|
||||||
|
const camera = {
|
||||||
|
id: "k1:sensor.camera.right",
|
||||||
|
sourceId: "sensor.camera.right",
|
||||||
|
modality: "video",
|
||||||
|
availability: "streaming",
|
||||||
|
transport: "websocket",
|
||||||
|
previewUrl: null,
|
||||||
|
delivery: {
|
||||||
|
id: "camera-preview-2",
|
||||||
|
kind: "mse-fmp4-websocket",
|
||||||
|
url: "/camera-preview/2",
|
||||||
|
mediaType: 'video/mp4; codecs="avc1.641028"',
|
||||||
|
},
|
||||||
|
activation: {
|
||||||
|
groupId: "k1:device-session:camera.preview.decoder",
|
||||||
|
maxActive: 1,
|
||||||
|
selected: true,
|
||||||
|
controllable: false,
|
||||||
|
},
|
||||||
|
binding: {
|
||||||
|
deviceId: "k1-a",
|
||||||
|
deviceSessionId: "device-session",
|
||||||
|
acquisitionId: "acquisition-quick",
|
||||||
|
},
|
||||||
|
capabilities: { defaultVisible: true, overlay: true },
|
||||||
|
};
|
||||||
|
const projected = pendingLiveCameraPresentationDiagnostics(
|
||||||
|
[camera],
|
||||||
|
new Set(),
|
||||||
|
new Set(),
|
||||||
|
);
|
||||||
|
assert.deepEqual(projected.map(({ event }) => event), [{
|
||||||
|
eventCode: "live_camera_source_projected",
|
||||||
|
streamId: "camera-preview-2",
|
||||||
|
}]);
|
||||||
|
|
||||||
|
const emitted = new Set(projected.map(({ key }) => key));
|
||||||
|
const admitted = pendingLiveCameraPresentationDiagnostics(
|
||||||
|
[camera],
|
||||||
|
new Set([camera.id]),
|
||||||
|
emitted,
|
||||||
|
);
|
||||||
|
assert.deepEqual(admitted.map(({ event }) => event), [{
|
||||||
|
eventCode: "live_camera_window_admitted",
|
||||||
|
streamId: "camera-preview-2",
|
||||||
|
}]);
|
||||||
|
admitted.forEach(({ key }) => emitted.add(key));
|
||||||
|
assert.deepEqual(
|
||||||
|
pendingLiveCameraPresentationDiagnostics([camera], new Set([camera.id]), emitted),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
pendingLiveCameraPresentationDiagnostics(
|
||||||
|
[{ ...camera, delivery: null }],
|
||||||
|
new Set([camera.id]),
|
||||||
|
new Set(),
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("workspace layout API uses the canonical endpoint and optimistic revision", async () => {
|
test("workspace layout API uses the canonical endpoint and optimistic revision", async () => {
|
||||||
const calls = [];
|
const calls = [];
|
||||||
const current = decodeObservationWorkspaceLayoutProfile(wireProfile());
|
const current = decodeObservationWorkspaceLayoutProfile(wireProfile());
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ LiveViewerEventCode = Literal[
|
|||||||
"live_receiver_recovery_exhausted",
|
"live_receiver_recovery_exhausted",
|
||||||
"live_receiver_active_store_admitted",
|
"live_receiver_active_store_admitted",
|
||||||
"live_receiver_error",
|
"live_receiver_error",
|
||||||
|
"live_camera_source_projected",
|
||||||
|
"live_camera_window_admitted",
|
||||||
"live_camera_transport_restart_requested",
|
"live_camera_transport_restart_requested",
|
||||||
"live_camera_transport_playing",
|
"live_camera_transport_playing",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -244,6 +244,28 @@ def test_live_viewer_diagnostic_endpoint_accepts_only_bounded_events(
|
|||||||
assert caplog.records[-1].camera_media_source_state == "open"
|
assert caplog.records[-1].camera_media_source_state == "open"
|
||||||
assert caplog.records[-1].camera_video_error_code == 3
|
assert caplog.records[-1].camera_video_error_code == 3
|
||||||
|
|
||||||
|
for event_code in (
|
||||||
|
"live_camera_source_projected",
|
||||||
|
"live_camera_window_admitted",
|
||||||
|
):
|
||||||
|
boundary_event = LiveViewerDiagnosticEvent(
|
||||||
|
schema_version="missioncore.live-viewer-diagnostic/v2",
|
||||||
|
event_code=event_code,
|
||||||
|
ui_build_id=expected_build,
|
||||||
|
document_instance_id="00000000-0000-4000-8000-000000000001",
|
||||||
|
viewer_instance_id="00000000-0000-4000-8000-000000000004",
|
||||||
|
lifecycle_generation=1,
|
||||||
|
stream_id="camera-preview-2",
|
||||||
|
)
|
||||||
|
with caplog.at_level(
|
||||||
|
logging.INFO,
|
||||||
|
logger="k1link.device_plugins.xgrids_k1.viewer_receiver",
|
||||||
|
):
|
||||||
|
boundary_response = endpoint(boundary_event)
|
||||||
|
assert boundary_response.status_code == 204
|
||||||
|
assert caplog.records[-1].event_code == event_code
|
||||||
|
assert caplog.records[-1].stream_id == "camera-preview-2"
|
||||||
|
|
||||||
|
|
||||||
def test_live_viewer_diagnostic_rejects_stale_build_before_logging(
|
def test_live_viewer_diagnostic_rejects_stale_build_before_logging(
|
||||||
caplog: pytest.LogCaptureFixture,
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
|||||||
Reference in New Issue
Block a user