Rename MJPEG streams when camera name changes (#136)

* Rename MJPEG streams when camera name changes

* Change camera name to HTTP request

This allows us to wait for it to for sure be done

* Fix reload logic

* whee lnt

* Reload on backend connect too

* Update CameraAndPipelineSelect.vue
This commit is contained in:
Matt
2020-10-16 16:48:24 -07:00
committed by GitHub
parent e37fcdea98
commit 31013346c0
10 changed files with 273 additions and 200 deletions

View File

@@ -198,6 +198,7 @@ import Logs from "./views/LogsView"
};
this.$options.sockets.onopen = () => {
this.$store.state.backendConnected = true;
this.$store.state.connectedCallbacks.forEach(it => it())
};
let closed = () => {

View File

@@ -14,6 +14,11 @@
name: "CvImage",
// eslint-disable-next-line vue/require-prop-types
props: ['address', 'scale', 'maxHeight', 'maxHeightMd', 'maxHeightXl', 'colorPicking', 'id', 'disconnected'],
data() {
return {
seed: 1.0,
}
},
computed: {
styleObject: {
get() {
@@ -41,9 +46,17 @@
},
src: {
get() {
return this.disconnected ? require("../../assets/noStream.jpg") : this.address;
return this.disconnected ? require("../../assets/noStream.jpg") : this.address + "?" + this.seed // This prevents caching
},
},
},
mounted() {
this.reload(); // Force reload image on creation
},
methods: {
reload() {
this.seed = new Date().getTime();
}
},
}
</script>

View File

@@ -34,7 +34,7 @@
:hover="true"
text="edit"
tooltip="Edit camera name"
@click="toCameraNameChange"
@click="changeCameraName"
/>
<div v-else>
<CVicon
@@ -292,13 +292,23 @@
}
},
methods: {
toCameraNameChange() {
changeCameraName() {
this.newCameraName = this.$store.getters.cameraList[this.currentCameraIndex];
this.isCameraNameEdit = true;
},
saveCameraNameChange() {
if (this.checkCameraName === "") {
this.handleInputWithIndex("changeCameraName", this.newCameraName);
// this.handleInputWithIndex("changeCameraName", this.newCameraName);
this.axios.post('http://' + this.$address + '/api/setCameraNickname',
{name: this.newCameraName, cameraIndex: this.$store.getters.currentCameraIndex})
// eslint-disable-next-line
.then(r => {
this.$emit('camera-name-changed')
})
.catch(e => {
console.log("HTTP error while changing camera name " + e);
this.$emit('camera-name-changed')
})
this.discardCameraNameChange();
}
},

View File

@@ -15,6 +15,7 @@ export default new Vuex.Store({
},
state: {
backendConnected: false,
connectedCallbacks: [],
colorPicking: false,
logsOverlay: false,
compactMode: localStorage.getItem("compactMode") === undefined ? undefined : localStorage.getItem("compactMode") === "true", // Compact mode is initially unset on purpose

View File

@@ -45,8 +45,9 @@
style="height: 100%;"
>
<div style="position: relative; width: 100%; height: 100%;">
<cvImage
<cv-image
:id="idx === 0 ? 'normal-stream' : ''"
ref="streams"
:address="$store.getters.streamAddress[idx]"
:disconnected="!$store.state.backendConnected"
scale="100"
@@ -71,7 +72,10 @@
<v-card
color="primary"
>
<camera-and-pipeline-select />
<!-- <v-btn @click="onCamNameChange">-->
<!-- Reload-->
<!-- </v-btn>-->
<camera-and-pipeline-select @camera-name-changed="reloadStreams" />
</v-card>
<v-card
:disabled="$store.getters.isDriverMode || $store.state.colorPicking"
@@ -212,8 +216,12 @@
</v-card-title>
<v-card-text>
Because the current resolution {{ this.$store.getters.videoFormatList[this.$store.getters.currentPipelineSettings.cameraVideoModeIndex].width }}
x {{ this.$store.getters.videoFormatList[this.$store.getters.currentPipelineSettings.cameraVideoModeIndex].height }}
Because the current resolution {{
this.$store.getters.videoFormatList[this.$store.getters.currentPipelineSettings.cameraVideoModeIndex].width
}}
x {{
this.$store.getters.videoFormatList[this.$store.getters.currentPipelineSettings.cameraVideoModeIndex].height
}}
is not yet calibrated, 3D mode cannot be enabled. Please
<a
href="/#/cameras"
@@ -240,202 +248,209 @@
</template>
<script>
import CameraAndPipelineSelect from "../components/pipeline/CameraAndPipelineSelect";
import cvImage from '../components/common/cv-image';
import InputTab from './PipelineViews/InputTab';
import ThresholdTab from './PipelineViews/ThresholdTab';
import ContoursTab from './PipelineViews/ContoursTab';
import OutputTab from './PipelineViews/OutputTab';
import TargetsTab from "./PipelineViews/TargetsTab";
import PnPTab from './PipelineViews/PnPTab';
import CameraAndPipelineSelect from "../components/pipeline/CameraAndPipelineSelect";
import cvImage from '../components/common/cv-image';
import InputTab from './PipelineViews/InputTab';
import ThresholdTab from './PipelineViews/ThresholdTab';
import ContoursTab from './PipelineViews/ContoursTab';
import OutputTab from './PipelineViews/OutputTab';
import TargetsTab from "./PipelineViews/TargetsTab";
import PnPTab from './PipelineViews/PnPTab';
export default {
name: 'CameraTab',
components: {
CameraAndPipelineSelect,
cvImage,
InputTab,
ThresholdTab,
ContoursTab,
OutputTab,
TargetsTab,
PnPTab,
},
data() {
return {
selectedTabsData: [0, 0, 0, 0],
snackbar: false,
counterData: 0,
dialog: false,
processingModeOverride: false
export default {
name: 'CameraTab',
components: {
CameraAndPipelineSelect,
cvImage,
InputTab,
ThresholdTab,
ContoursTab,
OutputTab,
TargetsTab,
PnPTab,
},
data() {
return {
selectedTabsData: [0, 0, 0, 0],
snackbar: false,
counterData: 0,
dialog: false,
processingModeOverride: false
}
},
computed: {
selectedTabs: {
get() {
return this.$store.getters.isDriverMode ? [0] : this.selectedTabsData;
},
set(value) {
this.selectedTabsData = value;
}
},
computed: {
selectedTabs: {
get() {
return this.$store.getters.isDriverMode ? [0] : this.selectedTabsData;
},
set(value) {
this.selectedTabsData = value;
}
},
tabGroups: {
get() {
let tabs = {
input: {
name: "Input",
component: "InputTab",
},
threshold: {
name: "Threshold",
component: "ThresholdTab",
},
contours: {
name: "Contours",
component: "ContoursTab",
},
output: {
name: "Output",
component: "OutputTab",
},
targets: {
name: "Target Info",
component: "TargetsTab",
},
pnp: {
name: "3D",
component: "PnPTab",
}
};
// 2D array of tab names and component names; each sub-array is a separate tab group
let ret = [];
if (this.$vuetify.breakpoint.smAndDown || this.$store.getters.isDriverMode || (this.$vuetify.breakpoint.mdAndDown && !this.$store.state.compactMode)) {
// One big tab group with all the tabs
ret[0] = Object.values(tabs);
} else if (this.$vuetify.breakpoint.mdAndDown || !this.$store.state.compactMode) {
// Two tab groups, one with "input, threshold, contours, output" and the other with "target info, 3D"
ret[0] = [tabs.input, tabs.threshold, tabs.contours, tabs.output];
ret[1] = [tabs.targets, tabs.pnp];
} else if (this.$vuetify.breakpoint.lgAndDown) {
// Three tab groups, one with "input", one with "threshold, contours, output", and the other with "target info, 3D"
ret[0] = [tabs.input];
ret[1] = [tabs.threshold, tabs.contours, tabs.output];
ret[2] = [tabs.targets, tabs.pnp];
} else if (this.$vuetify.breakpoint.xl) {
// Three tab groups, one with "input", one with "threshold, contours", and the other with "output, target info, 3D"
ret[0] = [tabs.input];
ret[1] = [tabs.threshold];
ret[2] = [tabs.contours, tabs.output]
ret[3] = [tabs.targets, tabs.pnp];
tabGroups: {
get() {
let tabs = {
input: {
name: "Input",
component: "InputTab",
},
threshold: {
name: "Threshold",
component: "ThresholdTab",
},
contours: {
name: "Contours",
component: "ContoursTab",
},
output: {
name: "Output",
component: "OutputTab",
},
targets: {
name: "Target Info",
component: "TargetsTab",
},
pnp: {
name: "3D",
component: "PnPTab",
}
};
// 2D array of tab names and component names; each sub-array is a separate tab group
let ret = [];
if (this.$vuetify.breakpoint.smAndDown || this.$store.getters.isDriverMode || (this.$vuetify.breakpoint.mdAndDown && !this.$store.state.compactMode)) {
// One big tab group with all the tabs
ret[0] = Object.values(tabs);
} else if (this.$vuetify.breakpoint.mdAndDown || !this.$store.state.compactMode) {
// Two tab groups, one with "input, threshold, contours, output" and the other with "target info, 3D"
ret[0] = [tabs.input, tabs.threshold, tabs.contours, tabs.output];
ret[1] = [tabs.targets, tabs.pnp];
} else if (this.$vuetify.breakpoint.lgAndDown) {
// Three tab groups, one with "input", one with "threshold, contours, output", and the other with "target info, 3D"
ret[0] = [tabs.input];
ret[1] = [tabs.threshold, tabs.contours, tabs.output];
ret[2] = [tabs.targets, tabs.pnp];
} else if (this.$vuetify.breakpoint.xl) {
// Three tab groups, one with "input", one with "threshold, contours", and the other with "output, target info, 3D"
ret[0] = [tabs.input];
ret[1] = [tabs.threshold];
ret[2] = [tabs.contours, tabs.output]
ret[3] = [tabs.targets, tabs.pnp];
}
return ret;
}
},
processingMode: {
get() {
return (this.$store.getters.currentPipelineSettings.solvePNPEnabled || this.processingModeOverride) ? 1 : 0;
},
set(value) {
if (this.$store.getters.isCalibrated) {
this.$store.getters.currentPipelineSettings.solvePNPEnabled = value === 1;
this.handlePipelineUpdate("solvePNPEnabled", value === 1);
}
}
},
driverMode: {
get() {
return this.$store.getters.isDriverMode;
},
set(value) {
this.$store.getters.currentCameraSettings.currentPipelineIndex = value ? -1 : 0;
this.handleInputWithIndex('currentPipeline', value ? -1 : 0);
}
},
selectedOutputs: {
// All this logic exists to deal with the reality that the output select buttons sometimes need an array and sometimes need a number (depending on whether or not they're exclusive)
get() {
// We switch the selector to single-select only on sm-and-down size devices, so we have to return a Number instead of an Array in that state
let ret;
if (this.$store.state.colorPicking) {
ret = [0]; // We want the input stream only while color picking
} else if (!this.$store.getters.isDriverMode) {
ret = this.$store.state.selectedOutputs || [0];
} else {
ret = [1]; // We want the output stream in driver mode
}
if (this.$vuetify.breakpoint.mdAndUp) {
return ret;
} else {
return ret[0] || 0;
}
},
processingMode: {
get() {
return (this.$store.getters.currentPipelineSettings.solvePNPEnabled || this.processingModeOverride) ? 1 : 0;
},
set(value) {
if (this.$store.getters.isCalibrated) {
this.$store.getters.currentPipelineSettings.solvePNPEnabled = value === 1;
this.handlePipelineUpdate("solvePNPEnabled", value === 1);
}
}
},
driverMode: {
get() {
return this.$store.getters.isDriverMode;
},
set(value) {
this.$store.getters.currentCameraSettings.currentPipelineIndex = value ? -1 : 0;
this.handleInputWithIndex('currentPipeline', value ? -1 : 0);
}
},
selectedOutputs: {
// All this logic exists to deal with the reality that the output select buttons sometimes need an array and sometimes need a number (depending on whether or not they're exclusive)
get() {
// We switch the selector to single-select only on sm-and-down size devices, so we have to return a Number instead of an Array in that state
let ret;
if (this.$store.state.colorPicking) {
ret = [0]; // We want the input stream only while color picking
} else if (!this.$store.getters.isDriverMode) {
ret = this.$store.state.selectedOutputs || [0];
} else {
ret = [1]; // We want the output stream in driver mode
}
if (this.$vuetify.breakpoint.mdAndUp) {
return ret;
} else {
return ret[0] || 0;
}
},
set(value) {
let valToCommit = [0];
if (value instanceof Array) {
// Value is already an array, we don't need to do anything
value.sort(); // Sort for visual consistency
valToCommit = value;
} else if (value) {
// Value is assumed to be a number, so we wrap it into an array
valToCommit = [value];
}
this.$store.commit("selectedOutputs", valToCommit);
// TODO: Currently the backend just sends both streams regardless of the selected outputs value, so we don't need to send anything
// this.handlePipelineUpdate('selectedOutputs', valToCommit);
}
},
latency: {
get() {
return this.$store.getters.currentPipelineResults.latency;
set(value) {
let valToCommit = [0];
if (value instanceof Array) {
// Value is already an array, we don't need to do anything
value.sort(); // Sort for visual consistency
valToCommit = value;
} else if (value) {
// Value is assumed to be a number, so we wrap it into an array
valToCommit = [value];
}
this.$store.commit("selectedOutputs", valToCommit);
// TODO: Currently the backend just sends both streams regardless of the selected outputs value, so we don't need to send anything
// this.handlePipelineUpdate('selectedOutputs', valToCommit);
}
},
methods: {
isCalibrated() {
const resolution = this.$store.getters.videoFormatList[this.$store.getters.currentPipelineSettings.cameraVideoModeIndex];
return this.$store.getters.currentCameraSettings.calibrations
.some(e => e.width === resolution.width && e.height === resolution.height)
},
onImageClick(event) {
// Only run on the input stream
if (event.target.alt !== "Stream0") return;
// Get a reference to the threshold tab (if it is shown) and call its "onClick" method
let ref = this.$refs["Threshold"];
if (ref && ref[0])
ref[0].onClick(event)
},
on3DClick() {
if (!this.$store.getters.isCalibrated) {
this.dialog = true;
this.processingModeOverride = true;
}
},
closeUncalibratedDialog() {
this.dialog = false;
this.processingModeOverride = false;
// this.$store.getters.currentPipelineSettings.solvePNPEnabled = false;
this.handlePipelineUpdate("solvePNPEnabled", false);
latency: {
get() {
return this.$store.getters.currentPipelineResults.latency;
}
}
},
created() {
this.$store.state.connectedCallbacks.push(this.reloadStreams)
},
methods: {
reloadStreams() {
// Reload the streams as we technically close and reopen them
this.$refs.streams.forEach(it => it.reload())
},
isCalibrated() {
const resolution = this.$store.getters.videoFormatList[this.$store.getters.currentPipelineSettings.cameraVideoModeIndex];
return this.$store.getters.currentCameraSettings.calibrations
.some(e => e.width === resolution.width && e.height === resolution.height)
},
onImageClick(event) {
// Only run on the input stream
if (event.target.alt !== "Stream0") return;
// Get a reference to the threshold tab (if it is shown) and call its "onClick" method
let ref = this.$refs["Threshold"];
if (ref && ref[0])
ref[0].onClick(event)
},
on3DClick() {
if (!this.$store.getters.isCalibrated) {
this.dialog = true;
this.processingModeOverride = true;
}
},
closeUncalibratedDialog() {
this.dialog = false;
this.processingModeOverride = false;
// this.$store.getters.currentPipelineSettings.solvePNPEnabled = false;
this.handlePipelineUpdate("solvePNPEnabled", false);
}
}
}
</script>
<style scoped>
.v-btn-toggle.fill {
width: 100%;
height: 100%;
}
.v-btn-toggle.fill {
width: 100%;
height: 100%;
}
.v-btn-toggle.fill > .v-btn {
width: 50%;
height: 100%;
}
.v-btn-toggle.fill > .v-btn {
width: 50%;
height: 100%;
}
th {
width: 80px;
text-align: center;
}
th {
width: 80px;
text-align: center;
}
</style>

View File

@@ -160,6 +160,20 @@ public class RequestHandler {
}
}
public static void setCameraNickname(Context ctx) {
try {
var data = kObjectMapper.readValue(ctx.body(), HashMap.class);
String name = String.valueOf(data.get("name"));
int idx = Integer.parseInt(String.valueOf(data.get("cameraIndex")));
VisionModuleManager.getInstance().getModule(idx).setCameraNickname(name);
ctx.status(200);
return;
} catch (JsonProcessingException e) {
e.printStackTrace();
}
ctx.status(500);
}
public static void uploadPnpModel(Context ctx) {
UITargetData data;
try {

View File

@@ -81,6 +81,7 @@ public class Server {
app.post("api/restartProgram", RequestHandler::restartProgram);
app.post("api/vision/pnpModel", RequestHandler::uploadPnpModel);
app.post("api/sendMetrics", RequestHandler::sendMetrics);
app.post("api/setCameraNickname", RequestHandler::setCameraNickname);
app.start(port);
}

View File

@@ -34,12 +34,12 @@ import org.photonvision.vision.frame.FrameDivisor;
public class MJPGFrameConsumer {
private final CvSource cvSource;
private final MjpegServer mjpegServer;
private CvSource cvSource;
private MjpegServer mjpegServer;
private FrameDivisor divisor = FrameDivisor.NONE;
@SuppressWarnings("FieldCanBeLocal")
private final VideoListener listener;
private VideoListener listener;
private final NetworkTable table;
@@ -163,4 +163,14 @@ public class MJPGFrameConsumer {
return "Unknown";
}
}
public void close() {
table.getEntry("connected").setBoolean(false);
mjpegServer.close();
cvSource.close();
listener.close();
mjpegServer = null;
cvSource = null;
listener = null;
}
}

View File

@@ -135,6 +135,11 @@ public class VisionModule {
saveAndBroadcastAll();
}
private void destroyStreams() {
dashboardInputStreamer.close();
dashboardOutputStreamer.close();
}
private void createStreams() {
var camStreamIdx = visionSource.getSettables().getConfiguration().streamIndex;
// If idx = 0, we want (1181, 1182)
@@ -143,11 +148,10 @@ public class VisionModule {
dashboardOutputStreamer =
new MJPGFrameConsumer(
visionSource.getSettables().getConfiguration().uniqueName + "-output",
outputStreamPort);
visionSource.getSettables().getConfiguration().nickname + "-output", outputStreamPort);
dashboardInputStreamer =
new MJPGFrameConsumer(
visionSource.getSettables().getConfiguration().uniqueName + "-input", inputStreamPort);
visionSource.getSettables().getConfiguration().nickname + "-input", inputStreamPort);
}
void setDriverMode(boolean isDriverMode) {
@@ -280,17 +284,22 @@ public class VisionModule {
OutgoingUIEvent.wrappedOf("mutatePipeline", propertyName, value, originContext));
}
void setCameraNickname(String newName) {
public void setCameraNickname(String newName) {
visionSource.getSettables().getConfiguration().nickname = newName;
ntConsumer.updateCameraNickname(newName);
// rename streams
fpsLimitedResultConsumers.clear();
// Teardown and recreate streams
destroyStreams();
createStreams();
fpsLimitedResultConsumers.add(result -> dashboardInputStreamer.accept(result.inputFrame));
fpsLimitedResultConsumers.add(result -> dashboardOutputStreamer.accept(result.outputFrame));
// Push new data to the UI
saveAndBroadcastAll();
}
public PhotonConfiguration.UICameraConfiguration toUICameraConfig() {

View File

@@ -62,12 +62,11 @@ public class VisionModuleChangeSubscriber extends DataChangeSubscriber {
// special case for non-PipelineSetting changes
switch (propName) {
case "cameraNickname": // rename camera
var newNickname = (String) newPropValue;
logger.info("Changing nickname to " + newNickname);
parentModule.setCameraNickname(newNickname);
parentModule.saveAndBroadcastAll();
return;
// case "cameraNickname": // rename camera
// var newNickname = (String) newPropValue;
// logger.info("Changing nickname to " + newNickname);
// parentModule.setCameraNickname(newNickname);
// return;
case "pipelineName": // rename current pipeline
logger.info("Changing nick to " + newPropValue);
parentModule.pipelineManager.getCurrentPipelineSettings().pipelineNickname =