diff --git a/api/Server.ts b/api/Server.ts
index 7b6f3065..5ff370c1 100755
--- a/api/Server.ts
+++ b/api/Server.ts
@@ -149,6 +149,12 @@ const checkS3Connection = async (): Promise => {
"Access-Control-Allow-Headers",
"where, offset, limit, Authorization, Origin, X-Requested-With, Content-Type, Accept, Viewport, if-none-match, cache-control"
);
+
+ // NOTE: We've seen an instance where the HOST request header is rewritten by the client, which would otherwise break
+ // some things. If the host is unknown, default to browse-next.
+ if (!request.headers.host.includes("cacophony.org.nz")) {
+ request.headers.host = "https://browse-next.cacophony.org.nz";
+ }
next();
});
await initialiseApi(app);
diff --git a/api/api/V1/recordingUtil.ts b/api/api/V1/recordingUtil.ts
index ad4c51da..1d9b0ea1 100755
--- a/api/api/V1/recordingUtil.ts
+++ b/api/api/V1/recordingUtil.ts
@@ -1356,6 +1356,9 @@ export function signedToken(
export const guessMimeType = (type, filename): string => {
const mimeType = mime.getType(filename);
if (mimeType) {
+ if (mimeType === "audio/x-aac") {
+ return "audio/mp4";
+ }
return mimeType;
}
switch (type) {
diff --git a/browse-next/src/api/Device.ts b/browse-next/src/api/Device.ts
index 691c257a..5ccf122c 100644
--- a/browse-next/src/api/Device.ts
+++ b/browse-next/src/api/Device.ts
@@ -341,6 +341,7 @@ export const getLatestStatusRecordingForDevice = (
params.append("max-results", "1");
params.append("types", "thermal");
params.append("include-false-positives", true.toString());
+ params.append("devices", deviceId.toString());
if (use2SecondRecordings) {
params.append("status-recordings", true.toString());
}
diff --git a/browse-next/src/components/CptvSingleFrame.vue b/browse-next/src/components/CptvSingleFrame.vue
index 6895223c..4914e3d0 100644
--- a/browse-next/src/components/CptvSingleFrame.vue
+++ b/browse-next/src/components/CptvSingleFrame.vue
@@ -120,6 +120,8 @@ const loadRecording = async () => {
);
frameData.value = new ImageData(buffer, 160, 120);
renderFrame();
+ } else {
+ break;
}
}
}
diff --git a/browse-next/src/components/DeviceRecordingSetup.vue b/browse-next/src/components/DeviceRecordingSetup.vue
index 6006131e..3f14cab6 100644
--- a/browse-next/src/components/DeviceRecordingSetup.vue
+++ b/browse-next/src/components/DeviceRecordingSetup.vue
@@ -4,11 +4,13 @@ import { selectedProjectDevices } from "@models/provides.ts";
import type {
ApiDeviceHistorySettings,
ApiDeviceResponse,
+ AudioModes,
} from "@typedefs/api/device";
import { useRoute } from "vue-router";
import type { DeviceId } from "@typedefs/api/common";
import type { LoadedResource } from "@api/types.ts";
import {
+ getDeviceModel,
getDeviceNodeGroup,
getSettingsForDevice,
updateDeviceSettings,
@@ -16,13 +18,12 @@ import {
import Datepicker from "@vuepic/vue-datepicker";
import { projectDevicesLoaded } from "@models/LoggedInUser.ts";
import { resourceIsLoading } from "@/helpers/utils.ts";
-import { DeviceType } from "@typedefs/api/consts.ts";
type Time = { hours: number; minutes: number; seconds: number };
const devices = inject(selectedProjectDevices) as Ref<
ApiDeviceResponse[] | null
>;
const route = useRoute();
-const saltNodeGroup = ref>(null);
+const deviceModel = ref>(null);
// Device Settings
const settings = ref>(null);
const syncedSettings = ref>(null);
@@ -53,9 +54,9 @@ const device = computed(() => {
const settingsLoading = resourceIsLoading(settings);
const lastSyncedSettingsLoading = resourceIsLoading(lastSyncedSettings);
-const nodeGroupInfoLoading = resourceIsLoading(saltNodeGroup);
+const nodeGroupInfoLoading = resourceIsLoading(deviceModel);
const isTc2Device = computed(() => {
- return (saltNodeGroup.value || "").includes("tc2");
+ return deviceModel.value === "tc2";
});
const defaultWindows = {
powerOn: "-30m",
@@ -152,8 +153,8 @@ const loadResource = async (
const initialised = ref(false);
onBeforeMount(async () => {
await projectDevicesLoaded();
- await loadResource(saltNodeGroup, () => getDeviceNodeGroup(deviceId.value));
await loadResource(settings, fetchSettings);
+ await loadResource(deviceModel, () => getDeviceModel(deviceId.value));
initialised.value = true;
if (settings.value && !settings.value.synced) {
// Load last synced settings
@@ -283,6 +284,233 @@ const customRecordingWindowStop = computed
+
+
+
+
Set Audio Recording Settings
+
+ Audio recordings are made 32 times a day for one minute at
+ random intervals.
+
+
+
+
+
+
+
+
+
{{ audioModeExplanation }}
+
+
+
@@ -181,6 +209,7 @@ import {
import DeviceApi from "@/api/Device.api";
import {
ApiDeviceHistorySettings,
+ AudioModes,
WindowsSettings,
} from "@typedefs/api/device";
@@ -205,6 +234,7 @@ export default defineComponent({
];
const showCustomModal = ref(false);
+
const fetchSettings = async () => {
try {
const response = await DeviceApi.getSettingsForDevice(props.deviceId);
@@ -330,9 +360,9 @@ export default defineComponent({
return rows;
});
- const saltNodeGroup = ref(null);
+ const deviceModel = ref<"pi" | "tc2">();
const isTc2Device = computed(() => {
- return (saltNodeGroup.value || "").includes("tc2");
+ return deviceModel.value === "tc2";
});
const defaultWindows = {
powerOn: "-30m",
@@ -340,6 +370,44 @@ export default defineComponent({
startRecording: "-30m",
stopRecording: "+30m",
};
+ const savingAudioSettings = ref(false);
+ const audioMode = computed({
+ get: () => {
+ return settings.value?.audioRecording?.audioMode ?? "Disabled";
+ },
+ set: async (val: AudioModes) => {
+ if (settings.value) {
+ settings.value.audioRecording = {
+ ...settings.value.audioRecording,
+ audioMode: val,
+ updated: new Date().toISOString(),
+ };
+ savingAudioSettings.value = true;
+ await DeviceApi.updateDeviceSettings(props.deviceId, settings.value);
+ savingAudioSettings.value = false;
+ }
+ },
+ });
+ const audioModeExplanation = computed(() => {
+ debugger;
+ switch (audioMode.value) {
+ case "AudioOnly":
+ return "Records audio in a 24-hour window and disables thermal recording.";
+ case "AudioOrThermal":
+ return "Records audio outside of the thermal recording window.";
+ case "AudioAndThermal":
+ return "Records audio in a 24-hour window; however, the camera cannot record during the 1 minute of audio recording.";
+ default:
+ return "";
+ }
+ });
+ // Audio Mode Options
+ const audioModeOptions = [
+ { value: "Disabled", text: "Disabled" },
+ { value: "AudioOnly", text: "Audio Only" },
+ { value: "AudioAndThermal", text: "Audio and Thermal" },
+ { value: "AudioOrThermal", text: "Audio or Thermal" },
+ ];
const recordingWindowSetting = computed<"default" | "always" | "custom">({
get: () => {
const s = settings.value as ApiDeviceHistorySettings;
@@ -500,17 +568,32 @@ export default defineComponent({
});
const initialized = ref(false);
+ watch(
+ () => audioMode.value,
+ async () => {
+ if (settings.value) {
+ savingAudioSettings.value = true;
+ await DeviceApi.updateDeviceSettings(props.deviceId, settings.value);
+ savingAudioSettings.value = false;
+ }
+ }
+ );
+
onMounted(async () => {
await fetchSettings();
initialized.value = true;
- const nodeGroupRes = await DeviceApi.getDeviceNodeGroup(props.deviceId);
- if (nodeGroupRes) {
- saltNodeGroup.value = nodeGroupRes;
+ const res = await DeviceApi.getDeviceModel(props.deviceId);
+ if (res) {
+ deviceModel.value = res;
}
});
return {
isTc2Device,
+ savingAudioSettings,
+ audioMode,
+ audioModeExplanation,
+ audioModeOptions,
settings,
currentWindowsType,
fields,
diff --git a/types/api/device.d.ts b/types/api/device.d.ts
index a948f74d..1bea3614 100644
--- a/types/api/device.d.ts
+++ b/types/api/device.d.ts
@@ -59,6 +59,17 @@ export type ThermalRecordingSettings = {
useLowPowerMode: boolean;
} & SettingsBase;
+export type AudioModes =
+ | "Disabled"
+ | "AudioOnly"
+ | "AudioAndThermal"
+ | "AudioOrThermal";
+
+export type AudioRecordingSettings = {
+ audioMode?: AudioModes;
+ audioSeed?: number;
+} & SettingsBase;
+
export type WindowsSettings = {
startRecording: string;
stopRecording: string;
@@ -82,6 +93,7 @@ export interface ApiDeviceHistorySettings {
maskRegions?: MaskRegions;
ratThresh?: any;
thermalRecording?: ThermalRecordingSettings;
+ audioRecording?: AudioRecordingSettings;
windows?: WindowsSettings;
synced?: boolean;
}