diff --git a/src/Components/Patient/models.tsx b/src/Components/Patient/models.tsx
index 343c80a60bf..8002a86442f 100644
--- a/src/Components/Patient/models.tsx
+++ b/src/Components/Patient/models.tsx
@@ -1,18 +1,26 @@
import { ConsultationModel, PatientCategory } from "../Facility/models";
import { PerformedByModel } from "../HCX/misc";
import {
+ APPETITE_CHOICES,
+ BLADDER_DRAINAGE_CHOICES,
+ BLADDER_ISSUE_CHOICES,
+ BOWEL_ISSUE_CHOICES,
CONSCIOUSNESS_LEVEL,
HEARTBEAT_RHYTHM_CHOICES,
HumanBodyRegion,
INSULIN_INTAKE_FREQUENCY_OPTIONS,
LIMB_RESPONSE_OPTIONS,
NURSING_CARE_PROCEDURES,
+ NUTRITION_ROUTE_CHOICES,
OCCUPATION_TYPES,
+ ORAL_ISSUE_CHOICES,
OXYGEN_MODALITY_OPTIONS,
PressureSoreExudateAmountOptions,
PressureSoreTissueTypeOptions,
RATION_CARD_CATEGORY,
RESPIRATORY_SUPPORT,
+ SLEEP_CHOICES,
+ URINATION_FREQUENCY_CHOICES,
VENTILATOR_MODE_OPTIONS,
} from "../../Common/constants";
@@ -257,6 +265,7 @@ export interface SampleListModel {
export const DailyRoundTypes = [
"NORMAL",
+ "COMMUNITY_NURSES_LOG",
"DOCTORS_LOG",
"VENTILATOR",
"AUTOMATED",
@@ -355,6 +364,8 @@ export interface DailyRoundsModel {
infusions?: NameQuantity[];
iv_fluids?: NameQuantity[];
output?: NameQuantity[];
+ total_intake_calculated?: number;
+ total_output_calculated?: number;
ventilator_spo2?: number;
ventilator_interface?: (typeof RESPIRATORY_SUPPORT)[number]["value"];
ventilator_oxygen_modality?: (typeof OXYGEN_MODALITY_OPTIONS)[number]["value"];
@@ -370,6 +381,15 @@ export interface DailyRoundsModel {
ventilator_tidal_volume?: number;
pressure_sore?: IPressureSore[];
+ bowel_issue?: (typeof BOWEL_ISSUE_CHOICES)[number];
+ bladder_drainage?: (typeof BLADDER_DRAINAGE_CHOICES)[number];
+ bladder_issue?: (typeof BLADDER_ISSUE_CHOICES)[number];
+ is_experiencing_dysuria?: boolean;
+ urination_frequency?: (typeof URINATION_FREQUENCY_CHOICES)[number];
+ sleep?: (typeof SLEEP_CHOICES)[number];
+ nutrition_route?: (typeof NUTRITION_ROUTE_CHOICES)[number];
+ oral_issue?: (typeof ORAL_ISSUE_CHOICES)[number];
+ appetite?: (typeof APPETITE_CHOICES)[number];
}
export interface FacilityNameModel {
diff --git a/src/Locale/en/Common.json b/src/Locale/en/Common.json
index 432d5979bed..b0316e4d98c 100644
--- a/src/Locale/en/Common.json
+++ b/src/Locale/en/Common.json
@@ -166,6 +166,8 @@
"not_specified": "Not Specified",
"all_changes_have_been_saved": "All changes have been saved",
"no_data_found": "No data found",
+ "other_details": "Other details",
+ "no_remarks": "No remarks",
"edit": "Edit",
"clear_selection": "Clear selection",
"select_date": "Select date",
@@ -203,6 +205,6 @@
"deleted_successfully": "{{name}} deleted successfully",
"delete_item": "Delete {{name}}",
"unsupported_browser": "Unsupported Browser",
- "unsupported_browser_description": "Your browser ({{name}} version {{version}}) is not supported. Please update your browser to the latest version or switch to a supported browser for the best experience."
-
+ "unsupported_browser_description": "Your browser ({{name}} version {{version}}) is not supported. Please update your browser to the latest version or switch to a supported browser for the best experience.",
+ "add_remarks": "Add remarks"
}
\ No newline at end of file
diff --git a/src/Locale/en/Consultation.json b/src/Locale/en/Consultation.json
index 3d101bcb2bd..d12a6cb16ac 100644
--- a/src/Locale/en/Consultation.json
+++ b/src/Locale/en/Consultation.json
@@ -1,4 +1,19 @@
{
+ "CONSULTATION_TAB__UPDATES": "Overview",
+ "CONSULTATION_TAB__FEED": "Feed",
+ "CONSULTATION_TAB__SUMMARY": "Vitals",
+ "CONSULTATION_TAB__ABG": "ABG",
+ "CONSULTATION_TAB__MEDICINES": "Medicines",
+ "CONSULTATION_TAB__FILES": "Files",
+ "CONSULTATION_TAB__INVESTIGATIONS": "Investigations",
+ "CONSULTATION_TAB__NEUROLOGICAL_MONITORING": "Neuro",
+ "CONSULTATION_TAB__VENTILATOR": "Ventilation",
+ "CONSULTATION_TAB__NUTRITION": "Nutrition",
+ "CONSULTATION_TAB__PRESSURE_SORE": "Pressure Sore",
+ "CONSULTATION_TAB__NURSING": "Nursing",
+ "CONSULTATION_TAB__DIALYSIS": "Dialysis",
+ "CONSULTATION_TAB__ABDM": "ABDM Records",
+ "nursing_information": "Nursing Information",
"no_consultation_updates": "No consultation updates",
"consultation_updates": "Consultation updates",
"update_log": "Update Log",
diff --git a/src/Locale/en/LogUpdate.json b/src/Locale/en/LogUpdate.json
index 080e2fc979a..f057c8c4235 100644
--- a/src/Locale/en/LogUpdate.json
+++ b/src/Locale/en/LogUpdate.json
@@ -1,4 +1,36 @@
{
+ "LOG_UPDATE_CREATED_NOTIFICATION": "{{ roundType }} created successfully",
+ "LOG_UPDATE_UPDATED_NOTIFICATION": "{{ roundType }} updated successfully",
+ "LOG_UPDATE_FIELD_LABEL__rounds_type": "Rounds Type",
+ "LOG_UPDATE_FIELD_LABEL__patient_category": "Category",
+ "LOG_UPDATE_FIELD_LABEL__consciousness_level": "Level of Consciousness",
+ "LOG_UPDATE_FIELD_LABEL__sleep": "Sleep",
+ "LOG_UPDATE_FIELD_LABEL__bowel_issue": "Bowel",
+ "LOG_UPDATE_FIELD_LABEL__bladder_drainage": "Drainage",
+ "LOG_UPDATE_FIELD_LABEL__bladder_issue": "Issues",
+ "LOG_UPDATE_FIELD_LABEL__is_experiencing_dysuria": "Experiences Dysuria?",
+ "LOG_UPDATE_FIELD_LABEL__urination_frequency": "Frequency of Urination",
+ "LOG_UPDATE_FIELD_LABEL__nutrition_route": "Nutrition Route",
+ "LOG_UPDATE_FIELD_LABEL__oral_issue": "Oral issues",
+ "LOG_UPDATE_FIELD_LABEL__appetite": "Appetite",
+ "LOG_UPDATE_FIELD_LABEL__physical_examination_info": "Physical Examination Info",
+ "LOG_UPDATE_FIELD_LABEL__bp": "Blood Pressure",
+ "LOG_UPDATE_FIELD_LABEL__blood_sugar_level": "Blood Sugar Level",
+ "LOG_UPDATE_FIELD_LABEL__action": "Action",
+ "LOG_UPDATE_FIELD_LABEL__review_interval": "Review after",
+ "LOG_UPDATE_FIELD_LABEL__rhythm": "Heartbeat Rhythm",
+ "LOG_UPDATE_FIELD_LABEL__rhythm_detail": "Rhythm Description",
+ "LOG_UPDATE_FIELD_LABEL__ventilator_spo2": "SpOтВВ",
+ "LOG_UPDATE_FIELD_LABEL__resp": "Respiratory Rate",
+ "LOG_UPDATE_FIELD_LABEL__temperature": "Temperature",
+ "LOG_UPDATE_FIELD_LABEL__other_details": "Other details",
+ "LOG_UPDATE_FIELD_LABEL__pulse": "Pulse",
+ "ROUNDS_TYPE__NORMAL": "Brief Update",
+ "ROUNDS_TYPE__COMMUNITY_NURSES_LOG": "Community Nurse's Log",
+ "ROUNDS_TYPE__VENTILATOR": "Detailed Update",
+ "ROUNDS_TYPE__DOCTORS_LOG": "Progress Note",
+ "ROUNDS_TYPE__AUTOMATED": "Virtual Nursing Assistant",
+ "ROUNDS_TYPE__TELEMEDICINE": "Tele-medicine Log",
"RESPIRATORY_SUPPORT_SHORT__UNKNOWN": "None",
"RESPIRATORY_SUPPORT_SHORT__OXYGEN_SUPPORT": "O2 Support",
"RESPIRATORY_SUPPORT_SHORT__NON_INVASIVE": "NIV",
@@ -20,11 +52,46 @@
"CONSCIOUSNESS_LEVEL__ALERT": "Alert",
"CONSCIOUSNESS_LEVEL__AGITATED_OR_CONFUSED": "Agitated or Confused",
"CONSCIOUSNESS_LEVEL__ONSET_OF_AGITATION_AND_CONFUSION": "Onset of Agitation and Confusion",
+ "BOWEL_ISSUE__NO_DIFFICULTY": "No difficulty",
+ "BOWEL_ISSUE__CONSTIPATION": "Constipation",
+ "BOWEL_ISSUE__DIARRHOEA": "Diarrhoea",
+ "BLADDER_DRAINAGE__NORMAL": "Normal",
+ "BLADDER_DRAINAGE__CONDOM_CATHETER": "Condom Catheter",
+ "BLADDER_DRAINAGE__DIAPER": "Diaper",
+ "BLADDER_DRAINAGE__INTERMITTENT_CATHETER": "Intermittent Catheter",
+ "BLADDER_DRAINAGE__CONTINUOUS_INDWELLING_CATHETER": "Continuous Indwelling Catheter",
+ "BLADDER_DRAINAGE__CONTINUOUS_SUPRAPUBIC_CATHETER": "Continuous Suprapubic Catheter",
+ "BLADDER_DRAINAGE__UROSTOMY": "Urostomy",
+ "BLADDER_ISSUE__NO_ISSUES": "No issues",
+ "BLADDER_ISSUE__INCONTINENCE": "Incontinence",
+ "BLADDER_ISSUE__RETENTION": "Retention",
+ "BLADDER_ISSUE__HESITANCY": "Hesitancy",
+ "URINATION_FREQUENCY__NORMAL": "Normal",
+ "URINATION_FREQUENCY__DECREASED": "Decreased",
+ "URINATION_FREQUENCY__INCREASED": "Increased",
+ "SLEEP__EXCESSIVE": "Excessive",
+ "SLEEP__SATISFACTORY": "Satisfactory",
+ "SLEEP__UNSATISFACTORY": "Unsatisfactory",
+ "SLEEP__NO_SLEEP": "No sleep",
+ "NUTRITION_ROUTE__ORAL": "Oral",
+ "NUTRITION_ROUTE__RYLES_TUBE": "Ryle's Tube",
+ "NUTRITION_ROUTE__GASTROSTOMY_OR_JEJUNOSTOMY": "Gastrostomy / Jejunostomy",
+ "NUTRITION_ROUTE__PEG": "PEG",
+ "NUTRITION_ROUTE__PARENTERAL_TUBING_FLUID": "Parenteral Tubing (Fluid)",
+ "NUTRITION_ROUTE__PARENTERAL_TUBING_TPN": "Parenteral Tubing (TPN)",
+ "ORAL_ISSUE__NO_ISSUE": "No issues",
+ "ORAL_ISSUE__DYSPHAGIA": "Dysphagia",
+ "ORAL_ISSUE__ODYNOPHAGIA": "Odynophagia",
+ "APPETITE__INCREASED": "Increased",
+ "APPETITE__SATISFACTORY": "Satisfactory",
+ "APPETITE__REDUCED": "Reduced",
+ "APPETITE__NO_TASTE_FOR_FOOD": "No taste for food",
+ "APPETITE__CANNOT_BE_ASSESSED": "Cannot be assessed",
"PUPIL_REACTION__UNKNOWN": "Unknown",
"PUPIL_REACTION__BRISK": "Brisk",
"PUPIL_REACTION__SLUGGISH": "Sluggish",
"PUPIL_REACTION__FIXED": "Fixed",
- "PUPIL_REACTION__CANNOT_BE_ASSESSED": "Cannot Be Assessed",
+ "PUPIL_REACTION__CANNOT_BE_ASSESSED": "Cannot be assessed",
"LIMB_RESPONSE__UNKNOWN": "Unknown",
"LIMB_RESPONSE__STRONG": "Strong",
"LIMB_RESPONSE__MODERATE": "Moderate",
@@ -56,18 +123,19 @@
"HEARTBEAT_RHYTHM__REGULAR": "Regular",
"HEARTBEAT_RHYTHM__IRREGULAR": "Irregular",
"HEARTBEAT_RHYTHM__UNKNOWN": "Unknown",
- "heartbeat_rhythm": "Heartbeat Rhythm",
- "heartbeat_description": "Heartbeat Description",
"blood_pressure": "Blood Pressure",
"map_acronym": "M.A.P.",
"systolic": "Systolic",
"diastolic": "Diastolic",
- "temperature": "Temperature",
- "resipiratory_rate": "Respiratory Rate",
"pain": "Pain",
"pain_chart_description": "Mark region and intensity of pain",
- "pulse": "Pulse",
"bradycardia": "Bradycardia",
"tachycardia": "Tachycardia",
- "spo2": "SpOтВВ"
+ "procedures_select_placeholder": "Select procedures to add details",
+ "oral_issue_for_non_oral_nutrition_route_error": "Can be specified only if nutrition route is set to Oral",
+ "routine": "Routine",
+ "bladder": "Bladder",
+ "nutrition": "Nutrition",
+ "vitals": "Vitals",
+ "nursing_care": "Nursing Care"
}
\ No newline at end of file
diff --git a/src/Locale/hi/LogUpdate.json b/src/Locale/hi/LogUpdate.json
index 3026eccce57..176886c855b 100644
--- a/src/Locale/hi/LogUpdate.json
+++ b/src/Locale/hi/LogUpdate.json
@@ -56,18 +56,12 @@
"HEARTBEAT_RHYTHM__REGULAR": "рдирд┐рдпрдорд┐рдд",
"HEARTBEAT_RHYTHM__IRREGULAR": "рдЕрдирд┐рдпрдорд┐рдд",
"HEARTBEAT_RHYTHM__UNKNOWN": "рдЕрдЬреНрдЮрд╛рдд",
- "heartbeat_rhythm": "рджрд┐рд▓ рдХреА рдзрдбрд╝рдХрди рдХреА рд▓рдп",
- "heartbeat_description": "рджрд┐рд▓ рдХреА рдзрдбрд╝рдХрди рдХрд╛ рд╡рд┐рд╡рд░рдг",
"blood_pressure": "рд░рдХреНрддрдЪрд╛рдк",
"map_acronym": "рдорд╛рдирдЪрд┐рддреНрд░",
"systolic": "рд╕рд┐рд╕реНрдЯреЛрд▓рд┐рдХ",
"diastolic": "рдбрд╛рдпрд╕реНрдЯреЛрд▓рд┐рдХ",
- "temperature": "рддрд╛рдкрдорд╛рди",
- "resipiratory_rate": "рд╢реНрд╡рд╕рди рджрд░",
"pain": "рджрд░реНрдж",
"pain_chart_description": "рджрд░реНрдж рдХрд╛ рдХреНрд╖реЗрддреНрд░ рдФрд░ рддреАрд╡реНрд░рддрд╛ рдЪрд┐рд╣реНрдирд┐рдд рдХрд░реЗрдВ",
- "pulse": "рдирд╛рдбрд╝реА",
"bradycardia": "рдордВрджрдирд╛рдбрд╝реА",
- "tachycardia": "tachycardia",
- "spo2": "SpOтВВ"
+ "tachycardia": "tachycardia"
}
\ No newline at end of file
diff --git a/src/Locale/kn/LogUpdate.json b/src/Locale/kn/LogUpdate.json
index e1d8104ad95..25e4ee4623e 100644
--- a/src/Locale/kn/LogUpdate.json
+++ b/src/Locale/kn/LogUpdate.json
@@ -56,18 +56,12 @@
"HEARTBEAT_RHYTHM__REGULAR": "р▓ир▓┐р▓пр▓ор▓┐р▓д",
"HEARTBEAT_RHYTHM__IRREGULAR": "р▓Ер▓ир▓┐р▓пр▓ор▓┐р▓д",
"HEARTBEAT_RHYTHM__UNKNOWN": "р▓Ер▓Ьр│Нр▓Юр▓╛р▓д",
- "heartbeat_rhythm": "р▓╣р│Гр▓жр▓п р▓мр▓бр▓┐р▓др▓ж р▓▓р▓п",
- "heartbeat_description": "р▓╣р│Гр▓жр▓п р▓мр▓бр▓┐р▓др▓ж р▓╡р▓┐р▓╡р▓░р▓гр│Ж",
"blood_pressure": "р▓░р▓Хр│Нр▓др▓жр│Кр▓др│Нр▓др▓б",
"map_acronym": "р▓ир▓Хр│Нр▓╖р│Ж",
"systolic": "р▓╕р▓┐р▓╕р│Нр▓Яр│Кр▓▓р▓┐р▓Хр│Н",
"diastolic": "р▓бр▓пр▓╛р▓╕р│Нр▓Яр│Кр▓▓р▓┐р▓Хр│Н",
- "temperature": "р▓др▓╛р▓кр▓ор▓╛р▓и",
- "resipiratory_rate": "р▓Йр▓╕р▓┐р▓░р▓╛р▓Яр▓ж р▓жр▓░",
"pain": "р▓ир│Лр▓╡р│Б",
"pain_chart_description": "р▓ир│Лр▓╡р▓┐р▓и р▓кр│Нр▓░р▓жр│Зр▓╢ р▓ор▓др│Нр▓др│Б р▓др│Ар▓╡р│Нр▓░р▓др│Жр▓пр▓ир│Нр▓ир│Б р▓Чр│Бр▓░р│Бр▓др▓┐р▓╕р▓┐",
- "pulse": "р▓ир▓╛р▓бр▓┐",
"bradycardia": "р▓мр│Нр▓░р▓╛р▓бр▓┐р▓Хр▓╛р▓░р│Нр▓бр▓┐р▓пр▓╛",
- "tachycardia": "р▓Яр▓╛р▓Хр▓┐р▓Хр▓╛р▓░р│Нр▓бр▓┐р▓пр▓╛",
- "spo2": "SpOтВВ"
+ "tachycardia": "р▓Яр▓╛р▓Хр▓┐р▓Хр▓╛р▓░р│Нр▓бр▓┐р▓пр▓╛"
}
\ No newline at end of file
diff --git a/src/Locale/ml/LogUpdate.json b/src/Locale/ml/LogUpdate.json
index 1a3304b88ca..d2503cd8d34 100644
--- a/src/Locale/ml/LogUpdate.json
+++ b/src/Locale/ml/LogUpdate.json
@@ -56,18 +56,12 @@
"HEARTBEAT_RHYTHM__REGULAR": "р┤кр┤др┤┐р┤╡р╡Н",
"HEARTBEAT_RHYTHM__IRREGULAR": "р┤Хр╡Нр┤░р┤ор┤░р┤╣р┤┐р┤др┤В",
"HEARTBEAT_RHYTHM__UNKNOWN": "р┤Ер┤Ьр╡Нр┤Юр┤╛р┤др┤В",
- "heartbeat_rhythm": "р┤╣р╡Гр┤жр┤пр┤ор┤┐р┤Яр┤┐р┤кр╡Нр┤кр╡Н р┤др┤╛р┤│р┤В",
- "heartbeat_description": "р┤╣р╡Гр┤жр┤пр┤ор┤┐р┤Яр┤┐р┤кр╡Нр┤кр╡Н р┤╡р┤┐р┤╡р┤░р┤гр┤В",
"blood_pressure": "р┤░р┤Хр╡Нр┤др┤╕р┤ор╡Нр┤ор╡╝р┤жр╡Нр┤жр┤В",
"map_acronym": "р┤ор┤╛р┤кр╡Нр┤кр╡Н",
"systolic": "р┤╕р┤┐р┤╕р╡Нр┤▒р╡Нр┤▒р╡Лр┤│р┤┐р┤Хр╡Н",
"diastolic": "р┤бр┤пр┤╕р╡Нр┤▒р╡Нр┤▒р╡Лр┤│р┤┐р┤Хр╡Н",
- "temperature": "р┤др┤╛р┤кр┤ир┤┐р┤▓",
- "resipiratory_rate": "р┤╢р╡Нр┤╡р┤╕р┤и р┤ир┤┐р┤░р┤Хр╡Нр┤Хр╡Н",
"pain": "р┤╡р╡Зр┤жр┤и",
"pain_chart_description": "р┤╡р╡Зр┤жр┤ир┤пр╡Бр┤Яр╡Ж р┤кр╡Нр┤░р┤жр╡Зр┤╢р┤╡р╡Бр┤В р┤др╡Ар┤╡р╡Нр┤░р┤др┤пр╡Бр┤В р┤Ер┤Яр┤пр┤╛р┤│р┤кр╡Нр┤кр╡Жр┤Яр╡Бр┤др╡Нр┤др╡Бр┤Х",
- "pulse": "р┤кр╡╛р┤╕р╡Н",
"bradycardia": "р┤мр╡Нр┤░р┤╛р┤бр┤┐р┤Хр┤╛р╡╝р┤бр┤┐р┤п",
- "tachycardia": "р┤Яр┤╛р┤Хр╡Нр┤Хр┤┐р┤Хр╡Нр┤Хр┤╛р╡╝р┤бр┤┐р┤п",
- "spo2": "SpOтВВ"
+ "tachycardia": "р┤Яр┤╛р┤Хр╡Нр┤Хр┤┐р┤Хр╡Нр┤Хр┤╛р╡╝р┤бр┤┐р┤п"
}
\ No newline at end of file
diff --git a/src/Locale/ta/LogUpdate.json b/src/Locale/ta/LogUpdate.json
index fa5308cde39..61a52f69d48 100644
--- a/src/Locale/ta/LogUpdate.json
+++ b/src/Locale/ta/LogUpdate.json
@@ -56,18 +56,12 @@
"HEARTBEAT_RHYTHM__REGULAR": "ро╡ро┤роХрпНроХрооро╛рой",
"HEARTBEAT_RHYTHM__IRREGULAR": "роТро┤рпБроЩрпНроХро▒рпНро▒",
"HEARTBEAT_RHYTHM__UNKNOWN": "родрпЖро░ро┐ропро╡ро┐ро▓рпНро▓рпИ",
- "heartbeat_rhythm": "роЗродропродрпН родрпБроЯро┐рокрпНрокрпБ родро╛ро│роорпН",
- "heartbeat_description": "роЗродроп родрпБроЯро┐рокрпНрокрпБ ро╡ро┐ро│роХрпНроХроорпН",
"blood_pressure": "роЗро░родрпНрод роЕро┤рпБродрпНродроорпН",
"map_acronym": "ро╡ро░рпИрокроЯроорпН",
"systolic": "роЪро┐ро╕рпНроЯро╛ро▓ро┐роХрпН",
"diastolic": "роЯропро╕рпНроЯро╛ро▓ро┐роХрпН",
- "temperature": "ро╡рпЖрокрпНрокроиро┐ро▓рпИ",
- "resipiratory_rate": "роЪрпБро╡ро╛роЪ ро╡ро┐роХро┐родроорпН",
"pain": "ро╡ро▓ро┐",
"pain_chart_description": "ро╡ро▓ро┐ропро┐ройрпН рокроХрпБродро┐ рооро▒рпНро▒рпБроорпН родрпАро╡ро┐ро░родрпНродрпИ роХрпБро▒ро┐роХрпНроХро╡рпБроорпН",
- "pulse": "родрпБроЯро┐рокрпНрокрпБ",
"bradycardia": "рокро┐ро░ро╛роЯро┐ роХро╛ро░рпНроЯро┐ропро╛",
- "tachycardia": "роЯро╛роХрпНро░ро┐роХрпНроХро╛ро░рпНроЯро┐ропро╛",
- "spo2": "SpOтВВ"
+ "tachycardia": "роЯро╛роХрпНро░ро┐роХрпНроХро╛ро░рпНроЯро┐ропро╛"
}
\ No newline at end of file
From 383dfbc899220a22e30e7530c40ce010b31314e4 Mon Sep 17 00:00:00 2001
From: Aakash Singh
Date: Wed, 18 Sep 2024 18:01:40 +0530
Subject: [PATCH 17/22] Revamp media player (#8437)
---
package-lock.json | 52 +-
package.json | 1 -
src/Common/hooks/useHLSPlayer.ts | 17 -
src/Common/hooks/useMSEplayer.ts | 236 ------
.../Assets/AssetType/ONVIFCamera.tsx | 12 +-
.../Assets/configure/CameraConfigure.tsx | 4 +-
src/Components/CameraFeed/CameraFeed.tsx | 176 ++---
.../CameraFeedOld.tsx} | 210 +++---
src/Components/CameraFeed/FeedAlert.tsx | 6 +-
.../CameraFeed/FeedNetworkSignal.tsx | 6 +-
src/Components/CameraFeed/routes.ts | 10 +-
.../CameraFeed}/useFeedPTZ.ts | 41 +-
src/Components/CameraFeed/useOperateCamera.ts | 5 +
src/Components/CameraFeed/usePlayer.tsx | 58 --
src/Components/CameraFeed/utils.ts | 7 +-
src/Components/CameraFeed/videoPlayer.tsx | 198 +++++
.../Facility/Consultations/Feed.tsx | 704 ------------------
.../Facility/Consultations/FeedButton.tsx | 63 --
18 files changed, 459 insertions(+), 1347 deletions(-)
delete mode 100644 src/Common/hooks/useHLSPlayer.ts
delete mode 100644 src/Common/hooks/useMSEplayer.ts
rename src/Components/{Facility/Consultations/LiveFeed.tsx => CameraFeed/CameraFeedOld.tsx} (83%)
rename src/{Common/hooks => Components/CameraFeed}/useFeedPTZ.ts (89%)
delete mode 100644 src/Components/CameraFeed/usePlayer.tsx
create mode 100644 src/Components/CameraFeed/videoPlayer.tsx
delete mode 100644 src/Components/Facility/Consultations/Feed.tsx
delete mode 100644 src/Components/Facility/Consultations/FeedButton.tsx
diff --git a/package-lock.json b/package-lock.json
index 14fd0262607..553224ffc27 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -44,7 +44,6 @@
"react-infinite-scroll-component": "^6.1.0",
"react-markdown": "^8.0.7",
"react-pdf": "^9.1.0",
- "react-player": "^2.16.0",
"react-redux": "^8.1.1",
"react-webcam": "^7.2.0",
"redux": "^4.2.1",
@@ -5937,12 +5936,12 @@
}
},
"node_modules/braces": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
- "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"dependencies": {
- "fill-range": "^7.0.1"
+ "fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
@@ -6947,6 +6946,7 @@
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
+ "dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -8377,9 +8377,9 @@
}
},
"node_modules/fill-range": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
- "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"dependencies": {
"to-regex-range": "^5.0.1"
@@ -10942,11 +10942,6 @@
"url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/load-script": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz",
- "integrity": "sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA=="
- },
"node_modules/loader-runner": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
@@ -12076,11 +12071,6 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/memoize-one": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
- "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="
- },
"node_modules/merge-refs": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/merge-refs/-/merge-refs-1.3.0.tgz",
@@ -14330,11 +14320,6 @@
"react": "^18.3.1"
}
},
- "node_modules/react-fast-compare": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
- "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="
- },
"node_modules/react-google-recaptcha": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/react-google-recaptcha/-/react-google-recaptcha-3.1.0.tgz",
@@ -14992,21 +14977,6 @@
}
}
},
- "node_modules/react-player": {
- "version": "2.16.0",
- "resolved": "https://registry.npmjs.org/react-player/-/react-player-2.16.0.tgz",
- "integrity": "sha512-mAIPHfioD7yxO0GNYVFD1303QFtI3lyyQZLY229UEAp/a10cSW+hPcakg0Keq8uWJxT2OiT/4Gt+Lc9bD6bJmQ==",
- "dependencies": {
- "deepmerge": "^4.0.0",
- "load-script": "^1.0.0",
- "memoize-one": "^5.1.1",
- "prop-types": "^15.7.2",
- "react-fast-compare": "^3.0.1"
- },
- "peerDependencies": {
- "react": ">=16.6.0"
- }
- },
"node_modules/react-redux": {
"version": "8.1.3",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.3.tgz",
@@ -15652,9 +15622,9 @@
}
},
"node_modules/requirejs": {
- "version": "2.3.6",
- "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.6.tgz",
- "integrity": "sha512-ipEzlWQe6RK3jkzikgCupiTbTvm4S0/CAU5GlgptkN5SO6F3u0UD0K18wy6ErDqiCyP4J4YYe1HuAShvsxePLg==",
+ "version": "2.3.7",
+ "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.7.tgz",
+ "integrity": "sha512-DouTG8T1WanGok6Qjg2SXuCMzszOo0eHeH9hDZ5Y4x8Je+9JB38HdTLT4/VA8OaUhBa0JPVHJ0pyBkM1z+pDsw==",
"dev": true,
"bin": {
"r_js": "bin/r.js",
diff --git a/package.json b/package.json
index 8dcd4e13954..cfc0b3389a2 100644
--- a/package.json
+++ b/package.json
@@ -79,7 +79,6 @@
"react-infinite-scroll-component": "^6.1.0",
"react-markdown": "^8.0.7",
"react-pdf": "^9.1.0",
- "react-player": "^2.16.0",
"react-redux": "^8.1.1",
"react-webcam": "^7.2.0",
"redux": "^4.2.1",
diff --git a/src/Common/hooks/useHLSPlayer.ts b/src/Common/hooks/useHLSPlayer.ts
deleted file mode 100644
index 32a52043fe7..00000000000
--- a/src/Common/hooks/useHLSPlayer.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import ReactPlayer from "react-player";
-import { IOptions } from "./useMSEplayer";
-
-export const useHLSPLayer = (ref: ReactPlayer | null) => {
- const startStream = ({ onSuccess, onError }: IOptions = {}) => {
- try {
- ref?.setState({ url: ref?.props.url + "&t=" + Date.now() });
- onSuccess && onSuccess(undefined);
- } catch (err) {
- onError && onError(err);
- }
- };
- return {
- startStream,
- stopStream: undefined,
- };
-};
diff --git a/src/Common/hooks/useMSEplayer.ts b/src/Common/hooks/useMSEplayer.ts
deleted file mode 100644
index 5271c08fd56..00000000000
--- a/src/Common/hooks/useMSEplayer.ts
+++ /dev/null
@@ -1,236 +0,0 @@
-import { useEffect, useRef } from "react";
-
-export interface IAsset {
- middlewareHostname: string;
-}
-
-interface UseMSEMediaPlayerOption {
- config: IAsset;
- url?: string;
- videoEl: HTMLVideoElement | null;
-}
-
-export interface ICameraAssetState {
- id: string;
- accessKey: string;
- middleware_address: string;
- location_middleware: string;
-}
-
-export enum StreamStatus {
- Playing,
- Stop,
- Loading,
- Offline,
-}
-
-interface UseMSEMediaPlayerReturnType {
- stopStream: (config: { id: string }, options: IOptions) => void;
- startStream: (options?: IOptions) => void;
-}
-
-export interface IOptions {
- onSuccess?: (resp: any) => void;
- onError?: (err: any) => void;
-}
-const stopStream =
- ({
- middlewareHostname,
- ws,
- }: {
- middlewareHostname: string;
- ws?: WebSocket;
- }) =>
- (payload: { id: string }, options: IOptions) => {
- const { id } = payload;
- ws?.close();
- fetch(`https://${middlewareHostname}/stop`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({ id }),
- })
- .then((res) => {
- if (!res.ok) {
- throw new Error("network response was not ok");
- }
- return res.json();
- })
- .then((res) => options?.onSuccess && options.onSuccess(res))
- .catch((err) => options.onError && options.onError(err));
- };
-
-/**
- * MSE player utility
- */
-const Utf8ArrayToStr = (array: string | any[] | Uint8Array) => {
- let out, i, c;
- let char2, char3;
- out = "";
- const len = array.length;
- i = 0;
- while (i < len) {
- c = array[i++];
- switch (c >> 4) {
- case 7:
- out += String.fromCharCode(c);
- break;
- case 13:
- char2 = array[i++];
- out += String.fromCharCode(((c & 0x1f) << 6) | (char2 & 0x3f));
- break;
- case 14:
- char2 = array[i++];
- char3 = array[i++];
- out += String.fromCharCode(
- ((c & 0x0f) << 12) | ((char2 & 0x3f) << 6) | ((char3 & 0x3f) << 0),
- );
- break;
- }
- }
- return out;
-};
-
-export const useMSEMediaPlayer = ({
- config,
- url,
- videoEl,
-}: UseMSEMediaPlayerOption): UseMSEMediaPlayerReturnType => {
- const mseQueue: any[] = [];
- let mseStreamingStarted = false;
- const wsRef = useRef();
- let mseSourceBuffer: any;
-
- const pushPacket = () => {
- if (!mseSourceBuffer.updating) {
- if (mseQueue.length > 0) {
- const packet = mseQueue.shift();
- // Check if SourceBuffer has been removed before appending buffer
- if (mseSourceBuffer.removed) {
- console.error("Attempted to append to a removed SourceBuffer.");
- return;
- }
- mseSourceBuffer.appendBuffer(packet);
- } else {
- mseStreamingStarted = false;
- }
- }
- if (videoEl && videoEl.buffered.length > 0) {
- if (typeof document.hidden !== "undefined" && document.hidden) {
- //no sound, browser paused video without sound in background
- videoEl.currentTime =
- videoEl.buffered.end(videoEl.buffered.length - 1) - 0.5;
- }
- }
- };
-
- const readPacket = (packet: any) => {
- if (!mseStreamingStarted) {
- // Check if SourceBuffer has been removed before appending buffer
- if (mseSourceBuffer.removed) {
- console.error("Attempted to append to a removed SourceBuffer.");
- return;
- }
- mseSourceBuffer.appendBuffer(packet);
- mseStreamingStarted = true;
- return;
- }
- mseQueue.push(packet);
- if (!mseSourceBuffer.updating) {
- pushPacket();
- }
- };
-
- const startStream = ({ onError, onSuccess }: IOptions = {}) => {
- // location.protocol == 'https:' ? protocol = 'wss' : protocol = 'ws';
- try {
- wsRef.current?.close();
- const mse = new MediaSource();
- if (videoEl) {
- videoEl.src = window.URL.createObjectURL(mse);
- }
-
- if (url) {
- mse.addEventListener(
- "sourceopen",
- function () {
- wsRef.current = new WebSocket(url);
- const ws = wsRef.current;
- ws.binaryType = "arraybuffer";
- ws.onopen = function (_event) {
- onSuccess && onSuccess(undefined);
- };
- ws.onmessage = function (event) {
- const data = new Uint8Array(event.data);
- if (+data[0] === 9) {
- const decoded_arr = data.slice(1);
- let mimeCodec;
- if (window.TextDecoder) {
- mimeCodec = new TextDecoder("utf-8").decode(decoded_arr);
- } else {
- mimeCodec = Utf8ArrayToStr(decoded_arr);
- }
- try {
- mseSourceBuffer = mse.addSourceBuffer(
- `video/mp4; codecs="${mimeCodec}"`,
- );
- } catch (error) {
- onError?.(error);
- return;
- }
- mseSourceBuffer.mode = "segments";
- if (mseQueue.length > 0 && !mseSourceBuffer.updating) {
- mseSourceBuffer.addEventListener("updateend", pushPacket);
- }
- } else {
- readPacket(event.data);
- }
- };
- ws.onerror = function (event) {
- onError && onError(event);
- };
- },
- false,
- );
- }
- } catch (e) {
- onError && onError(e);
- }
- };
-
- document.addEventListener("DOMContentLoaded", function () {
- if (videoEl) {
- videoEl.addEventListener("loadeddata", () => {
- videoEl.play();
- });
-
- //fix stalled video in safari
- videoEl.addEventListener("pause", () => {
- if (
- videoEl.currentTime >
- videoEl.buffered.end(videoEl.buffered.length - 1)
- ) {
- videoEl.currentTime =
- videoEl.buffered.end(videoEl.buffered.length - 1) - 0.1;
- videoEl.play();
- }
- });
-
- videoEl.addEventListener("error", (e) => {
- console.log("video_error", e);
- });
- }
- });
-
- useEffect(() => {
- return () => {
- wsRef.current?.close();
- };
- }, []);
-
- return {
- startStream: startStream,
- stopStream: stopStream({ ...config, ws: wsRef.current }),
- };
-};
diff --git a/src/Components/Assets/AssetType/ONVIFCamera.tsx b/src/Components/Assets/AssetType/ONVIFCamera.tsx
index 21beb5f7bf8..e49fcad549d 100644
--- a/src/Components/Assets/AssetType/ONVIFCamera.tsx
+++ b/src/Components/Assets/AssetType/ONVIFCamera.tsx
@@ -16,6 +16,7 @@ import routes from "../../../Redux/api";
import useQuery from "../../../Utils/request/useQuery";
import CareIcon from "../../../CAREUI/icons/CareIcon";
+import useOperateCamera from "../../CameraFeed/useOperateCamera";
interface Props {
assetId: string;
@@ -47,6 +48,8 @@ const ONVIFCamera = ({ assetId, facilityId, asset, onUpdated }: Props) => {
});
const authUser = useAuthUser();
+ const { operate } = useOperateCamera(assetId ?? "", true);
+
useEffect(() => {
if (asset) {
setAssetType(asset?.asset_class);
@@ -92,7 +95,6 @@ const ONVIFCamera = ({ assetId, facilityId, asset, onUpdated }: Props) => {
const addPreset = async (e: SyntheticEvent) => {
e.preventDefault();
- const config = getCameraConfig(asset as AssetData);
const data = {
bed_id: bed.id,
preset_name: newPreset,
@@ -100,13 +102,7 @@ const ONVIFCamera = ({ assetId, facilityId, asset, onUpdated }: Props) => {
try {
setLoadingAddPreset(true);
- const response = await fetch(
- `https://${resolvedMiddleware?.hostname}/status?hostname=${config.hostname}&port=${config.port}&username=${config.username}&password=${config.password}`,
- );
- if (!response.ok) {
- throw new Error("Network error");
- }
- const presetData = await response.json();
+ const { data: presetData } = await operate({ type: "get_status" });
const { res } = await request(routes.createAssetBed, {
body: {
diff --git a/src/Components/Assets/configure/CameraConfigure.tsx b/src/Components/Assets/configure/CameraConfigure.tsx
index 5a8ccd5c184..e5e017db8dc 100644
--- a/src/Components/Assets/configure/CameraConfigure.tsx
+++ b/src/Components/Assets/configure/CameraConfigure.tsx
@@ -1,6 +1,6 @@
import { SyntheticEvent } from "react";
import { AssetData } from "../AssetTypes";
-import LiveFeed from "../../Facility/Consultations/LiveFeed";
+import CameraFeedOld from "../../CameraFeed/CameraFeedOld";
import { BedSelect } from "../../Common/BedSelect";
import { BedModel } from "../../Facility/models";
import { getCameraConfig } from "../../../Utils/transformUtils";
@@ -76,7 +76,7 @@ export default function CameraConfigure(props: CameraConfigureProps) {
- (null);
+ const playerRef = useRef(null);
const playerWrapperRef = useRef(null);
- const streamUrl = getStreamUrl(props.asset);
+ const [streamUrl, setStreamUrl] = useState("");
const inlineControls = useBreakpoints({ default: false, sm: true });
- const player = usePlayer(streamUrl, playerRef);
-
const [isFullscreen, setFullscreen] = useFullscreen();
const [state, setState] = useState();
- useEffect(() => setState(player.status), [player.status, setState]);
-
+ const [playedOn, setPlayedOn] = useState();
+ const [playerStatus, setPlayerStatus] = useState("stop");
// Move camera when selected preset has changed
useEffect(() => {
async function move(preset: PTZPayload) {
@@ -73,21 +70,29 @@ export default function CameraFeed(props: Props) {
getPresets(props.onCameraPresetsObtained);
}, [props.operate, props.onCameraPresetsObtained]);
- const initializeStream = useCallback(() => {
- player.initializeStream({
- onSuccess: async () => {
- props.onStreamSuccess?.();
- const { res } = await props.operate({ type: "get_status" });
- if (res?.status === 500) {
- setState("host_unreachable");
+ const initializeStream = useCallback(async () => {
+ if (!playerRef.current) return;
+ setPlayerStatus("loading");
+ await props
+ .operate({ type: "get_stream_token" })
+ .then(({ res, data }) => {
+ if (res?.status != 200) {
+ setState("authentication_error");
+ return props.onStreamError?.();
}
- },
- onError: props.onStreamError,
- });
- }, [player.initializeStream]);
+ const result = data?.result as { token: string };
+ return setStreamUrl(getStreamUrl(props.asset, result.token));
+ })
+ .catch(() => {
+ setState("host_unreachable");
+ return props.onStreamError?.();
+ });
+ }, []);
// Start stream on mount
- useEffect(() => initializeStream(), [initializeStream]);
+ useEffect(() => {
+ initializeStream();
+ }, []);
const resetStream = () => {
setState("loading");
@@ -154,7 +159,7 @@ export default function CameraFeed(props: Props) {
isFullscreen ? "hidden lg:flex" : "flex",
"items-center justify-between px-4 py-0.5 transition-all duration-500 ease-in-out lg:py-1",
(() => {
- if (player.status !== "playing") {
+ if (playerStatus !== "playing") {
return "bg-black text-zinc-400";
}
@@ -168,7 +173,7 @@ export default function CameraFeed(props: Props) {
>
@@ -200,75 +205,74 @@ export default function CameraFeed(props: Props) {
{/* Notifications */}
- {player.status === "playing" &&
}
+ {playerStatus === "playing" &&
}
{/* No Feed informations */}
- {state === "host_unreachable" && (
-
- )}
- {player.status === "offline" && (
-
- )}
+ {(() => {
+ switch (state) {
+ case "host_unreachable":
+ return (
+
+ );
+ case "authentication_error":
+ return (
+
+ );
+ case "offline":
+ return (
+
+ );
+ }
+ })()}
{/* Video Player */}
- {isIOS ? (
-
- }
- controls={false}
- pip={false}
- playsinline
- playing
- muted
- width="100%"
- height="100%"
- onPlay={player.onPlayCB}
- onEnded={() => player.setStatus("stop")}
- onError={(e, _, hlsInstance) => {
- if (e === "hlsError") {
- const recovered = hlsInstance.recoverMediaError();
- console.info(recovered);
- }
- }}
- />
-
- ) : (
-
{!inlineControls && (
{
+import { BedSelect } from "../Common/BedSelect.js";
+import { BedModel } from "../Facility/models.js";
+import useWindowDimensions from "../../Common/hooks/useWindowDimensions.js";
+import CareIcon, { IconName } from "../../CAREUI/icons/CareIcon.js";
+import Page from "../Common/components/Page.js";
+import ConfirmDialog from "../Common/ConfirmDialog.js";
+import { FieldLabel } from "../Form/FormFields/FormField.js";
+import useFullscreen from "../../Common/hooks/useFullscreen.js";
+import TextFormField from "../Form/FormFields/TextFormField.js";
+import VideoPlayer from "./videoPlayer.js";
+
+export enum StreamStatus {
+ Playing,
+ Stop,
+ Loading,
+ Offline,
+}
+
+export const FeedCameraPTZHelpButton = (props: { cameraPTZ: CameraPTZ[] }) => {
+ const { cameraPTZ } = props;
+ return (
+
+ );
+};
+
+const CameraFeedOld = (props: any) => {
const middlewareHostname = props.middlewareHostname;
const [presetsPage, setPresetsPage] = useState(0);
const cameraAsset = props.asset;
@@ -57,21 +110,7 @@ const LiveFeed = (props: any) => {
const isExtremeSmallScreen =
width <= extremeSmallScreenBreakpoint ? true : false;
const liveFeedPlayerRef = useRef
(null);
-
- const videoEl = liveFeedPlayerRef.current as HTMLVideoElement;
-
- const streamUrl = isIOS
- ? `https://${middlewareHostname}/stream/${cameraAsset?.accessKey}/channel/0/hls/live/index.m3u8?uuid=${cameraAsset?.accessKey}&channel=0`
- : `wss://${middlewareHostname}/stream/${cameraAsset?.accessKey}/channel/0/mse?uuid=${cameraAsset?.accessKey}&channel=0`;
-
- const { startStream } = useMSEMediaPlayer({
- config: {
- middlewareHostname,
- ...cameraAsset,
- },
- url: streamUrl,
- videoEl,
- });
+ const [streamUrl, setStreamUrl] = useState("");
const refreshPresetsHash = props.refreshPresetsHash;
@@ -80,6 +119,7 @@ const LiveFeed = (props: any) => {
const {
absoluteMove,
getCameraStatus,
+ getStreamToken,
getPTZPayload,
getPresets,
gotoPreset,
@@ -199,12 +239,20 @@ const LiveFeed = (props: any) => {
}
}, [page.offset, cameraAsset.id, refreshPresetsHash]);
- const startStreamFeed = () => {
- startStream({
- onSuccess: () => setStreamStatus(StreamStatus.Playing),
- onError: () => setStreamStatus(StreamStatus.Offline),
+ const startStreamFeed = useCallback(async () => {
+ if (!liveFeedPlayerRef.current) return;
+
+ await getStreamToken({
+ onSuccess: (data) => {
+ setStreamUrl(
+ `wss://${middlewareHostname}/stream/${cameraAsset?.accessKey}/channel/0/mse?uuid=${cameraAsset?.accessKey}&channel=0&token=${data.token}`,
+ );
+ },
+ onError: () => {
+ setStreamStatus(StreamStatus.Offline);
+ },
});
- };
+ }, [liveFeedPlayerRef.current]);
const viewOptions = (page: number) => {
return presets
@@ -228,7 +276,7 @@ const LiveFeed = (props: any) => {
return () => {
clearTimeout(tId);
};
- }, [startStream, streamStatus]);
+ }, [startStreamFeed, streamStatus]);
const handlePagination = (cOffset: number) => {
setPage({
@@ -243,13 +291,10 @@ const LiveFeed = (props: any) => {
precision === 16 ? 1 : precision * 2,
);
},
- reset: () => {
+ reset: async () => {
setStreamStatus(StreamStatus.Loading);
setVideoStartTime(null);
- startStream({
- onSuccess: () => setStreamStatus(StreamStatus.Playing),
- onError: () => setStreamStatus(StreamStatus.Offline),
- });
+ await startStreamFeed();
},
fullScreen: () => {
if (!liveFeedPlayerRef.current) return;
@@ -364,56 +409,23 @@ const LiveFeed = (props: any) => {
- {/* ADD VIDEO PLAYER HERE */}
- {isIOS ? (
-
- }
- controls={false}
- playsinline
- playing
- muted
- width="100%"
- height="100%"
- onPlay={() => {
- setVideoStartTime(() => new Date());
- setStreamStatus(StreamStatus.Playing);
- }}
- onWaiting={() => {
- const delay = calculateVideoLiveDelay();
- if (delay > 5) {
- setStreamStatus(StreamStatus.Loading);
- }
- }}
- onError={(e, _, hlsInstance) => {
- if (e === "hlsError") {
- const recovered = hlsInstance.recoverMediaError();
- console.info(recovered);
- }
- }}
- />
-
- ) : (
-
- )}
+
{
+ setVideoStartTime(() => new Date());
+ }}
+ onWaiting={() => {
+ const delay = calculateVideoLiveDelay();
+ if (delay > 5) {
+ setStreamStatus(StreamStatus.Loading);
+ }
+ }}
+ onSuccess={() => setStreamStatus(StreamStatus.Playing)}
+ onError={() => setStreamStatus(StreamStatus.Offline)}
+ />
{streamStatus === StreamStatus.Playing &&
calculateVideoLiveDelay() > 3 && (
@@ -670,4 +682,4 @@ const LiveFeed = (props: any) => {
);
};
-export default LiveFeed;
+export default CameraFeedOld;
diff --git a/src/Components/CameraFeed/FeedAlert.tsx b/src/Components/CameraFeed/FeedAlert.tsx
index b907ce72ad2..09f3b21ae42 100644
--- a/src/Components/CameraFeed/FeedAlert.tsx
+++ b/src/Components/CameraFeed/FeedAlert.tsx
@@ -2,14 +2,15 @@ import { Transition } from "@headlessui/react";
import { useEffect, useState } from "react";
import CareIcon, { IconName } from "../../CAREUI/icons/CareIcon";
import { classNames } from "../../Utils/utils";
-import { StreamStatus } from "./usePlayer";
+export type StreamStatus = "playing" | "stop" | "loading" | "offline";
export type FeedAlertState =
| StreamStatus
| "moving"
| "zooming"
| "saving_preset"
- | "host_unreachable";
+ | "host_unreachable"
+ | "authentication_error";
interface Props {
state?: FeedAlertState;
@@ -24,6 +25,7 @@ const ALERT_ICON_MAP: Partial> = {
zooming: "l-search",
saving_preset: "l-save",
host_unreachable: "l-exclamation-triangle",
+ authentication_error: "l-exclamation-triangle",
};
export default function FeedAlert({ state }: Props) {
diff --git a/src/Components/CameraFeed/FeedNetworkSignal.tsx b/src/Components/CameraFeed/FeedNetworkSignal.tsx
index 68df86bb4d5..4b1e6c7fbe3 100644
--- a/src/Components/CameraFeed/FeedNetworkSignal.tsx
+++ b/src/Components/CameraFeed/FeedNetworkSignal.tsx
@@ -1,7 +1,7 @@
import { useEffect, useState } from "react";
import { calculateVideoDelay } from "./utils";
import NetworkSignal from "../../CAREUI/display/NetworkSignal";
-import { StreamStatus } from "./usePlayer";
+import { StreamStatus } from "./FeedAlert";
interface Props {
playerRef: React.RefObject;
@@ -23,7 +23,9 @@ export default function FeedNetworkSignal(props: Props) {
// 2. This value may become negative when the web-socket stream
// disconnects while the tab was not in focus.
if (-5 > delay || delay > 5) {
- props.onReset();
+ if (document.hasFocus() && props.status !== "loading") {
+ props.onReset();
+ }
}
}, 1000);
diff --git a/src/Components/CameraFeed/routes.ts b/src/Components/CameraFeed/routes.ts
index 482dc515eb3..aecbdc655fa 100644
--- a/src/Components/CameraFeed/routes.ts
+++ b/src/Components/CameraFeed/routes.ts
@@ -13,6 +13,12 @@ export type GetStatusResponse = {
};
};
+export type GetStreamTokenResponse = {
+ result: {
+ token: string;
+ };
+};
+
export type GetPresetsResponse = {
result: Record;
};
@@ -21,7 +27,9 @@ export const FeedRoutes = {
operateAsset: {
path: "/api/v1/asset/{id}/operate_assets/",
method: "POST",
- TRes: Type(),
+ TRes: Type<
+ GetStreamTokenResponse | GetStatusResponse | GetPresetsResponse
+ >(),
TBody: Type<{ action: OperationAction }>(),
},
} as const;
diff --git a/src/Common/hooks/useFeedPTZ.ts b/src/Components/CameraFeed/useFeedPTZ.ts
similarity index 89%
rename from src/Common/hooks/useFeedPTZ.ts
rename to src/Components/CameraFeed/useFeedPTZ.ts
index a393edc5922..fb704baf972 100644
--- a/src/Common/hooks/useFeedPTZ.ts
+++ b/src/Components/CameraFeed/useFeedPTZ.ts
@@ -17,33 +17,11 @@ interface PTZPayload {
zoom: number;
}
-export interface PTZState {
- x: number;
- y: number;
- zoom: number;
- precision: number;
-}
-
interface UseMSEMediaPlayerOption {
config: IAsset;
dispatch: any;
}
-export interface ICameraAssetState {
- id: string;
- username: string;
- password: string;
- hostname: string;
- port: number;
-}
-
-export enum StreamStatus {
- Playing,
- Stop,
- Loading,
- Offline,
-}
-
interface UseMSEMediaPlayerReturnType {
absoluteMove: (payload: PTZPayload, options: IOptions) => void;
relativeMove: (payload: PTZPayload, options: IOptions) => void;
@@ -53,6 +31,7 @@ interface UseMSEMediaPlayerReturnType {
value?: number,
) => PTZPayload;
getCameraStatus: (options: IOptions) => void;
+ getStreamToken: (options: IOptions) => void;
getPresets: (options: IOptions) => void;
gotoPreset: (payload: IGotoPresetPayload, options: IOptions) => void;
}
@@ -88,6 +67,23 @@ const getCameraStatus =
: options?.onError && options.onError(resp));
};
+const getStreamToken =
+ (config: IAsset, dispatch: any) =>
+ async (options: IOptions = {}) => {
+ if (!config.id) return;
+ const resp = await dispatch(
+ operateAsset(config.id, {
+ action: {
+ type: "get_stream_token",
+ },
+ }),
+ );
+ resp &&
+ (resp.status === 200
+ ? options?.onSuccess && options.onSuccess(resp.data.result)
+ : options?.onError && options.onError(resp));
+ };
+
const getPresets =
(config: IAsset, dispatch: any) =>
async (options: IOptions = {}) => {
@@ -205,6 +201,7 @@ export const useFeedPTZ = ({
relativeMove: relativeMove(config, dispatch),
getPTZPayload,
getCameraStatus: getCameraStatus(config, dispatch),
+ getStreamToken: getStreamToken(config, dispatch),
getPresets: getPresets(config, dispatch),
gotoPreset: gotoPreset(config, dispatch),
};
diff --git a/src/Components/CameraFeed/useOperateCamera.ts b/src/Components/CameraFeed/useOperateCamera.ts
index c0e35f90bf2..bfddbf5b887 100644
--- a/src/Components/CameraFeed/useOperateCamera.ts
+++ b/src/Components/CameraFeed/useOperateCamera.ts
@@ -33,6 +33,10 @@ interface RelativeMoveOperation {
data: PTZPayload;
}
+interface GetStreamToken {
+ type: "get_stream_token";
+}
+
interface ResetFeedOperation {
type: "reset";
}
@@ -43,6 +47,7 @@ export type OperationAction =
| GoToPresetOperation
| AbsoluteMoveOperation
| RelativeMoveOperation
+ | GetStreamToken
| ResetFeedOperation;
/**
diff --git a/src/Components/CameraFeed/usePlayer.tsx b/src/Components/CameraFeed/usePlayer.tsx
deleted file mode 100644
index 7f2dc088739..00000000000
--- a/src/Components/CameraFeed/usePlayer.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import { MutableRefObject, useCallback, useState } from "react";
-import ReactPlayer from "react-player";
-import { isIOS } from "../../Utils/utils";
-import { useHLSPLayer } from "../../Common/hooks/useHLSPlayer";
-import { IOptions, useMSEMediaPlayer } from "../../Common/hooks/useMSEplayer";
-
-export type StreamStatus = "playing" | "stop" | "loading" | "offline";
-
-export default function usePlayer(
- streamUrl: string,
- ref: MutableRefObject,
-) {
- const [playedOn, setPlayedOn] = useState();
- const [status, setStatus] = useState("stop");
-
- // Voluntarily disabling react-hooks/rules-of-hooks for this line as order of
- // hooks is maintained (since platform won't change in runtime)
- const _start = isIOS
- ? // eslint-disable-next-line react-hooks/rules-of-hooks
- useHLSPLayer(ref.current as ReactPlayer).startStream
- : // eslint-disable-next-line react-hooks/rules-of-hooks
- useMSEMediaPlayer({
- // Voluntarily set to "" as it's used by `stopStream` only (which is not
- // used by this hook)
- config: { middlewareHostname: "" },
- url: streamUrl,
- videoEl: ref.current as HTMLVideoElement,
- }).startStream;
-
- const initializeStream = useCallback(
- ({ onSuccess, onError }: IOptions) => {
- setPlayedOn(undefined);
- setStatus("loading");
- _start({
- onSuccess,
- onError: (args) => {
- setStatus("offline");
- onError?.(args);
- },
- });
- },
- [ref.current, streamUrl],
- );
-
- const onPlayCB = () => {
- // Voluntarily updating only if previously undefined (as this method may be invoked by the HTML video element on tab re-focus)
- setPlayedOn((prev) => (prev === undefined ? new Date() : prev));
- setStatus("playing");
- };
-
- return {
- status,
- setStatus,
- initializeStream,
- playedOn,
- onPlayCB,
- };
-}
diff --git a/src/Components/CameraFeed/utils.ts b/src/Components/CameraFeed/utils.ts
index f4c55f315ad..5556237d579 100644
--- a/src/Components/CameraFeed/utils.ts
+++ b/src/Components/CameraFeed/utils.ts
@@ -1,7 +1,6 @@
import { MutableRefObject } from "react";
import { AssetClass, AssetData } from "../Assets/AssetTypes";
import { getCameraConfig } from "../../Utils/transformUtils";
-import { isIOS } from "../../Utils/utils";
export const calculateVideoDelay = (
ref: MutableRefObject,
@@ -17,7 +16,7 @@ export const calculateVideoDelay = (
return playedDuration - video.currentTime;
};
-export const getStreamUrl = (asset: AssetData) => {
+export const getStreamUrl = (asset: AssetData, token?: string) => {
if (asset.asset_class !== AssetClass.ONVIF) {
throw "getStreamUrl can be invoked only for ONVIF Assets";
}
@@ -26,7 +25,5 @@ export const getStreamUrl = (asset: AssetData) => {
const host = asset.resolved_middleware?.hostname;
const uuid = config.accessKey;
- return isIOS
- ? `https://${host}/stream/${uuid}/channel/0/hls/live/index.m3u8?uuid=${uuid}&channel=0`
- : `wss://${host}/stream/${uuid}/channel/0/mse?uuid=${uuid}&channel=0`;
+ return `wss://${host}/stream/${uuid}/channel/0/mse?uuid=${uuid}&channel=0${token ? `&token=${token}` : ""}`;
};
diff --git a/src/Components/CameraFeed/videoPlayer.tsx b/src/Components/CameraFeed/videoPlayer.tsx
new file mode 100644
index 00000000000..9b6ad378d9f
--- /dev/null
+++ b/src/Components/CameraFeed/videoPlayer.tsx
@@ -0,0 +1,198 @@
+import { useEffect, useRef } from "react";
+
+declare const ManagedMediaSource: typeof MediaSource;
+
+function isIOSVersionLessThan18() {
+ const ua = navigator.userAgent;
+ if (/iPad|iPhone|iPod/.test(ua)) {
+ const iOSVersionMatch = ua.match(/OS (\d+)_?(\d+)?/);
+ if (iOSVersionMatch && parseInt(iOSVersionMatch[1], 10) < 18) {
+ return true;
+ }
+ }
+ return false;
+}
+
+function isSafariVersionLessThan17() {
+ const ua = navigator.userAgent;
+ if (/^((?!chrome|android).)*safari/i.test(ua)) {
+ const safariVersionMatch = ua.match(/Version\/(\d+)\.(\d+)/);
+ if (safariVersionMatch && parseInt(safariVersionMatch[1], 10) < 17) {
+ return true;
+ }
+ }
+ return false;
+}
+
+interface VideoPlayerProps {
+ playerRef: React.RefObject;
+ streamUrl: string;
+ className?: string;
+ onPlay?: () => void;
+ onEnded?: () => void;
+ onWaiting?: () => void;
+ onSuccess?: (resp: any) => void;
+ onError?: (err: any) => void;
+}
+
+export default function VideoPlayer(props: VideoPlayerProps) {
+ const wsRef = useRef();
+ const playerRef = props.playerRef;
+ let mediaSource: MediaSource;
+ let mseSourceBuffer: SourceBuffer;
+ let buf: Uint8Array;
+ let bufLen = 0;
+
+ const pushPacket = () => {
+ if (mseSourceBuffer.updating) return;
+
+ try {
+ if (bufLen > 0) {
+ // If there's data in the buffer to append
+ const data = buf.slice(0, bufLen);
+ bufLen = 0; // Reset buffer length
+ mseSourceBuffer.appendBuffer(data); // Append data to SourceBuffer
+ } else if (mseSourceBuffer.buffered && mseSourceBuffer.buffered.length) {
+ // If no new data to append, check if there's buffered data in SourceBuffer
+ const end =
+ mseSourceBuffer.buffered.end(mseSourceBuffer.buffered.length - 1) -
+ 15;
+ const start = mseSourceBuffer.buffered.start(0);
+ if (end > start) {
+ // Remove older data from the SourceBuffer to free up space
+ mseSourceBuffer.remove(start, end);
+ mediaSource.setLiveSeekableRange(end, end + 15);
+ }
+ }
+ } catch (e) {
+ console.debug(e);
+ props.onError?.(e);
+ }
+ };
+
+ const readPacket = (event: MessageEvent) => {
+ if (mseSourceBuffer.updating || bufLen > 0) {
+ // Buffer data if SourceBuffer is updating or buffer has data
+ const b = new Uint8Array(event.data);
+ buf.set(b, bufLen);
+ bufLen += b.byteLength;
+ } else {
+ try {
+ // Append data directly if SourceBuffer is ready
+ mseSourceBuffer.appendBuffer(event.data);
+ } catch (e) {
+ console.debug(e);
+ props.onError?.(e);
+ }
+ }
+ };
+
+ const cleanup = () => {
+ console.debug("Cleaning up video player");
+ if (wsRef.current) {
+ wsRef.current.close();
+ }
+ if (mseSourceBuffer) {
+ mseSourceBuffer.abort();
+ }
+ if (playerRef.current) {
+ playerRef.current.pause();
+ playerRef.current.src = "";
+ playerRef.current.srcObject = null;
+ }
+ };
+
+ const startHLS = () => {
+ console.debug("Broken os/browser, falling back to hls");
+ try {
+ if (!playerRef.current || !props.streamUrl) return;
+ const url = new URL(props.streamUrl);
+ if (url.protocol === "wss:") {
+ url.protocol = "https:";
+ }
+ url.pathname = url.pathname.replace("mse", "hls/live/index.m3u8");
+ playerRef.current.src = url.toString();
+ playerRef.current.onplaying = () => {
+ props.onSuccess?.(undefined);
+ };
+ } catch (err) {
+ console.debug(err);
+ props.onError?.(err);
+ }
+ };
+
+ const startMSE = () => {
+ try {
+ if (!playerRef.current || !props.streamUrl) return;
+ if (typeof ManagedMediaSource !== "undefined") {
+ mediaSource = new ManagedMediaSource();
+ playerRef.current.disableRemotePlayback = true;
+ playerRef.current.srcObject = mediaSource;
+ } else {
+ mediaSource = new MediaSource();
+ playerRef.current.src = URL.createObjectURL(mediaSource);
+ }
+ mediaSource.onsourceopen = function () {
+ const ws = new WebSocket(props.streamUrl);
+ wsRef.current = ws;
+ ws.binaryType = "arraybuffer";
+ ws.onopen = (_) => props.onSuccess?.(undefined);
+ ws.onerror = (event) => props.onError?.(event);
+ ws.onmessage = function (event) {
+ const data = new Uint8Array(event.data);
+ // First packet is the codec type
+ if (+data[0] === 9) {
+ const mimeCodec = new TextDecoder("utf-8").decode(data.slice(1));
+ try {
+ mseSourceBuffer = mediaSource.addSourceBuffer(
+ `video/mp4; codecs="${mimeCodec}"`,
+ );
+ } catch (error) {
+ props.onError?.(error);
+ return;
+ }
+ buf = new Uint8Array(2 * 1024 * 1024);
+ mseSourceBuffer.mode = "segments";
+ mseSourceBuffer.onupdateend = pushPacket;
+ // switch to readPacket after creating SourceBuffer
+ ws.onmessage = readPacket;
+ } else {
+ readPacket(event);
+ }
+ };
+ };
+ } catch (err) {
+ console.debug(err);
+ }
+ };
+
+ useEffect(() => {
+ // if the device is ios < 18 or safari < 17 then fallback to hls
+ if (isIOSVersionLessThan18() || isSafariVersionLessThan17()) {
+ startHLS();
+ } else {
+ startMSE();
+ }
+ return () => {
+ cleanup();
+ };
+ }, [props.streamUrl]);
+
+ return (
+ <>
+