Camera: Live MJPEG view with a MP4 Recording History

This widget allows you to view your live camera with the more compatible MJPEG format for the live stream, and when a MP4 recording is made via the ipCamera binding, it will allow the folder icon to be clicked to show the recordings to be reviewed. Useful to show the number of people that have rung your doorbell, or driven up your driveway allowing a quick review of the recordings.

MJPEG has low latency and good compatibility with browsers.
There is a HLS version of this widget that will give higher resolution at the expense of latency and less compatible.

Once you have seen all the recordings, you can click on the “X” button to clear the counter which will NOT delete the recordings. The X just resets the number of how many recordings that have been made since you last reviewed and cleared the counter. The video camera icon returns you to the live view. You can enter full screen mode if the recording contains something of interest.

Setup

To get this widget working you MUST name your items with the default naming that occurs when you use the OH3 feature “Add equipment to model” from the things channel page (found at very bottom of page). If you manually create items, then the following items will need to be named as follows and linked to the channels of the same name:

EquipmentName (this is the item you select in the widget’s setup)
EquipmentName_MP4_History
EquipmentName_MP4_History_Length

The Base Url config needs to be set to use your openHAB IP address and also be updated to include the cameras unique ID. This will look like this and will end with a /

http://192.168.1.2:8080/ipcamera/doorbell/

Resources

uid: CameraMjpegHistory
props:
  parameters:
    - context: item
      label: Select the Camera (Equipment)
      name: camera
      required: true
      type: TEXT
    - description: "example: http://192.168.1.2:8080/ipcamera/CameraUniqueID/"
      label: Base URL
      name: cameraBaseURL
      required: true
      type: TEXT
    - description: ON will use the lower bandwidth autofps.mjpeg stream otherwise
        ipcamera.mjpeg
      label: Use Lower Bandwidth Stream
      name: lowerBandwidth
      required: true
      type: BOOLEAN
    - default: "true"
      label: Show Equipment Controls
      name: showSettings
      required: false
      type: BOOLEAN
      advanced: true
    - default: "false"
      label: Show Audio Alarms
      name: showAudioAlarms
      required: false
      type: BOOLEAN
      advanced: true
    - default: "true"
      label: Show Motion Alarms
      name: showMotionAlarms
      required: false
      type: BOOLEAN
      advanced: true
    - default: "true"
      label: Show Recording Icon
      name: showRecording
      required: false
      type: BOOLEAN
      advanced: true
timestamp: Mar 10, 2025, 5:09:36 PM
component: f7-card
config:
  key: "=(vars.selected === undefined) ? Math.random() : Math.random() +
    vars.selected"
  style:
    --f7-card-margin-horizontal: 0px
    --f7-card-margin-vertical: 0px
    border-radius: 6px
    height: 9.4rem
    width: 15.2rem
slots:
  default:
    - component: oh-video-card
      config:
        hideControls: false
        startManually: false
        url: =props.cameraBaseURL + items[props.camera +
          '_MP4_History'].state.split(",")[(vars.selected || 0)] +".mp4"
        visible: =vars.archive == 2
    - component: oh-image-card
      config:
        style:
          border-radius: 6px
          height: auto
          margin: 0px
          width: 100%
        url: "=props.lowerBandwidth === true ? (props.cameraBaseURL + 'autofps.mjpeg') :
          (props.cameraBaseURL + 'ipcamera.mjpeg')"
        visible: =vars.archive != 2
    - component: f7-row
      config:
        class: no-gap
        style:
          position: absolute
          top: 0px
          width: 99%
          z-index: 2
      slots:
        default:
          - component: oh-button
            config:
              action: variable
              actionVariable: archive
              actionVariableValue: 2
              iconColor: white
              iconF7: folder
              iconSize: 28
              style:
                margin: -2px
              visible: "=(items[props.camera + '_MP4_History'].state !== NULL &&
                items[props.camera + '_MP4_History_Length'].state > 0 ?
                vars.archive !== 2 : false)"
              z-index: 2
          - component: Label
            config:
              style:
                color: white
                font-size: 9px
                left: 18px
                position: absolute
                top: 12px
                z-index: -1
              text: =items[props.camera+'_MP4_History_Length'].state
              visible: =vars.archive != 2 && items[props.camera+'_MP4_History_Length'].state >
                0
          - component: Label
            config:
              style:
                color: white
                font-size: 9px
                left: 12px
                position: absolute
                top: 9px
                z-index: -1
              text: =((vars.selected || 0)+1)
              visible: =vars.archive == 2
          - component: oh-button
            config:
              action: variable
              actionVariable: archive
              actionVariableValue: 1
              iconColor: white
              iconF7: videocam
              iconSize: 28
              style:
                margin: -2px
              visible: =(vars.archive == 2)
          - component: oh-button
            config:
              action: variable
              actionVariable: selected
              actionVariableValue: =(items[props.camera +'_MP4_History_Length'].state-1)
              iconColor: white
              iconF7: backward_end
              iconSize: 28
              style:
                margin: -2px
              visible: =vars.archive == 2 && items[props.camera+'_MP4_History_Length'].state >
                0
          - component: oh-button
            config:
              action: variable
              actionVariable: selected
              actionVariableValue: =((vars.selected || 0)+1 < items[props.camera
                +'_MP4_History_Length'].state)?((vars.selected ||
                0)+1):(items[props.camera +'_MP4_History_Length'].state-1)
              iconColor: white
              iconF7: arrowtriangle_left
              iconSize: 28
              style:
                margin: -2px
              visible: =vars.archive == 2 && items[props.camera+'_MP4_History_Length'].state >
                0
          - component: oh-button
            config:
              action: variable
              actionVariable: selected
              actionVariableValue: =((vars.selected || 0) != 0)?((vars.selected || 0)-1):(0)
              iconColor: white
              iconF7: arrowtriangle_right
              iconSize: 28
              style:
                margin: -2px
              visible: =vars.archive == 2 && items[props.camera+'_MP4_History_Length'].state >
                0
          - component: oh-button
            config:
              action: variable
              actionVariable: selected
              actionVariableValue: 0
              iconColor: white
              iconF7: forward_end
              iconSize: 28
              style:
                margin: -2px
              visible: =vars.archive == 2 && items[props.camera+'_MP4_History_Length'].state >
                0
          - component: oh-button
            config:
              action: toggle
              actionCommand: "0"
              actionItem: =props.camera+'_MP4_History_Length'
              iconColor: white
              iconF7: clear
              iconSize: 28
              style:
                margin: -2px
              tooltip: Clear cameras mp4 history
              visible: =vars.archive == 2 && items[props.camera+'_MP4_History_Length'].state >
                0
              visibleTo:
                - role:administrator
    - component: f7-row
      config:
        class: no-gap
        style:
          position: absolute
          top: 7.7rem
          width: 99%
          z-index: 2
      slots:
        default:
          - component: oh-link
            config:
              color: red
              iconF7: videocam_circle
              iconSize: 22
              style:
                left: 0.2rem
                opacity: 0.7
                position: absolute
              visible: =props.showRecording === true && items[props.camera +
                '_MP4_Recording'].state > 0 && vars.archive !== 2
          - component: oh-link
            config:
              iconF7: eye
              iconSize: 22
              style:
                color: white
                opacity: "=(items[props.camera + '_Motion_Alarm'].state === 'ON') ? '0.7' : '0'"
                position: absolute
                right: 0.2rem
              visible: =props.showMotionAlarms === true
          - component: oh-link
            config:
              iconF7: ear
              iconSize: 22
              style:
                color: white
                left: 1.2rem
                opacity: "=(items[props.camera + '_Audio_Alarm'].state === 'ON') ? '0.7' : '0'"
                position: absolute
              visible: =props.showAudioAlarms === true
2 Likes

Just updated the widget to work with the newer item naming structure as follows when you create equipment from a thing.
Items names need to be like this…
EquipmentName (this is the item you select in the widget’s setup)
EquipmentName_MP4_History
EquipmentName_MP4_History_Length

I’m running OH 4.2.2-release build with the current IP Camera binding.

Currently I do have integrated my “DoorBird” camera, which is ONVIF-compatible and is able to provide a MJPEG feed on their own. Is it still necessary to configure ffmpeg for the binding to work? at least using the marketplace widget “Camera: Clickable thumbnail opens to a larger stream” does not retrieve the stream.

My thing:

UID: ipcamera:onvif:BinderDoor
label: BinderDoor
thingTypeUID: ipcamera:onvif
configuration:
  mjpegOptions: -q:v 5 -r 2 -vf scale=640:-2 -update 1
  ipAddress: 192.168.78.30
  updateImageWhen: "0"
  onvifPort: 80
  gifPreroll: 0
  ipWhitelist: DISABLE
  mp4OutOptions: -c:v copy -c:a copy
  pollTime: 1000
  password: xxx
  port: 80
  snapshotOptions: -an -vsync vfr -q:v 2 -update 1
  ptzContinuous: false
  onvifMediaProfile: 0
  username: yyy
  hlsOutOptions: -strict -2 -f lavfi -i aevalsrc=0 -acodec aac -vcodec copy
    -hls_flags delete_segments -hls_time 2 -hls_list_size 4
  gifOutOptions: -r 2 -filter_complex
    scale=-2:360:flags=lanczos,setpts=0.5*PTS,split[o1][o2];[o1]palettegen[p];[o2]fifo[o3];[o3][p]paletteuse
channels:
  - id: startStream
    channelTypeUID: ipcamera:startStream
    label: Start HLS Stream
    description: Lower the delay to start casting the camera by creating the files
      non stop in case they are needed.
    configuration: {}
  - id: pollImage
    channelTypeUID: ipcamera:pollImage
    label: Poll Image
    description: This can be used to trigger snapshot updates when an external PIR,
      button or other form of sensor turns this channel ON.
    configuration: {}
  - id: image
    channelTypeUID: ipcamera:image
    label: Image
    description: Low frame rate image from your camera. Recommend this is NOT used
      unless you have large pollTime.
    configuration: {}
  - id: recordingGif
    channelTypeUID: ipcamera:recordingGif
    label: GIF Recording
    description: Indicates how long the recording will occur for and when the file
      is created, the channel will change to 0 by itself.
    configuration: {}
  - id: gifHistory
    channelTypeUID: ipcamera:gifHistory
    label: GIF History
    description: A history of the last GIFs created in a CSV formatted string.
    configuration: {}
  - id: gifHistoryLength
    channelTypeUID: ipcamera:gifHistoryLength
    label: GIF History Length
    description: How many GIFs are stored in the history.
    configuration: {}
  - id: recordingMp4
    channelTypeUID: ipcamera:recordingMp4
    label: MP4 Recording
    description: Indicates how long the recording will occur for and when the file
      is created, the channel will change to 0 by itself.
    configuration: {}
  - id: mp4History
    channelTypeUID: ipcamera:mp4History
    label: MP4 History
    description: A history of the last mp4 recordings created in a CSV formatted string.
    configuration: {}
  - id: mp4HistoryLength
    channelTypeUID: ipcamera:mp4HistoryLength
    label: MP4 History Length
    description: How many mp4 recordings are stored in the history.
    configuration: {}
  - id: lastMotionType
    channelTypeUID: ipcamera:lastMotionType
    label: Last Motion Type
    description: A string that contains the type of motion alarm that was last triggered.
    configuration: {}
  - id: ffmpegMotionControl
    channelTypeUID: ipcamera:ffmpegMotionControl
    label: Control FFmpeg Motion Alarm
    description: Enable/Disable the motion alarm and control the sensitivity.
    configuration: {}
  - id: ffmpegMotionAlarm
    channelTypeUID: ipcamera:ffmpegMotionAlarm
    label: FFmpeg Motion Alarm
    description: FFmpeg has detected motion.
    configuration: {}
  - id: thresholdAudioAlarm
    channelTypeUID: ipcamera:thresholdAudioAlarm
    label: Audio Alarm Threshold
    description: By moving this control you should be able to change how sensitive
      the audio alarm is to soft or loud noises.
    configuration: {}
  - id: audioAlarm
    channelTypeUID: ipcamera:audioAlarm
    label: Audio Alarm
    description: Audio has triggered an Alarm.
    configuration: {}
  - id: externalMotion
    channelTypeUID: ipcamera:externalMotion
    label: External Motion
    description: Use any external sensor like a ZWave PIR sensor to flag that the
      camera has motion in its field of view.
    configuration: {}
  - id: motionAlarm
    channelTypeUID: ipcamera:motionAlarm
    label: Motion Alarm
    description: Motion has been detected.
    configuration: {}
  - id: cellMotionAlarm
    channelTypeUID: ipcamera:cellMotionAlarm
    label: Cell Motion Alarm
    description: Cell based motion has been detected.
    configuration: {}
  - id: lineCrossingAlarm
    channelTypeUID: ipcamera:lineCrossingAlarm
    label: Line Crossing Alarm
    description: Motion has been detected.
    configuration: {}
  - id: fieldDetectionAlarm
    channelTypeUID: ipcamera:fieldDetectionAlarm
    label: Field Alarm
    description: Intrusion has detected movement. AKA Field Detection Alarm.
    configuration: {}
  - id: faceDetected
    channelTypeUID: ipcamera:faceDetected
    label: Face Detected Alarm
    description: A face has been detected.
    configuration: {}
  - id: parkingAlarm
    channelTypeUID: ipcamera:parkingAlarm
    label: Parking Alarm
    description: A car has triggered the Parking Detection.
    configuration: {}
  - id: itemLeft
    channelTypeUID: ipcamera:itemLeft
    label: Item Left Alarm
    description: An item has been left.
    configuration: {}
  - id: itemTaken
    channelTypeUID: ipcamera:itemTaken
    label: Item Taken Alarm
    description: An item may have been stolen.
    configuration: {}
  - id: tamperAlarm
    channelTypeUID: ipcamera:tamperAlarm
    label: Tamper Alarm
    description: Camera may be stolen or damaged.
    configuration: {}
  - id: tooDarkAlarm
    channelTypeUID: ipcamera:tooDarkAlarm
    label: Too Dark Alarm
    description: Image is too dark.
    configuration: {}
  - id: storageAlarm
    channelTypeUID: ipcamera:storageAlarm
    label: Storage Alarm
    description: An issue with the cameras storage has been reported.
    configuration: {}
  - id: sceneChangeAlarm
    channelTypeUID: ipcamera:sceneChangeAlarm
    label: Scene Change Alarm
    description: Camera may have been moved.
    configuration: {}
  - id: tooBrightAlarm
    channelTypeUID: ipcamera:tooBrightAlarm
    label: Too Bright Alarm
    description: Image is too bright.
    configuration: {}
  - id: humanAlarm
    channelTypeUID: ipcamera:humanAlarm
    label: Human Alarm
    description: A person has triggered the Human Detection.
    configuration: {}
  - id: animalAlarm
    channelTypeUID: ipcamera:animalAlarm
    label: Animal Alarm
    description: An animal has triggered the object detection.
    configuration: {}
  - id: carAlarm
    channelTypeUID: ipcamera:carAlarm
    label: Car Alarm
    description: A car has triggered the Vehicle Detection.
    configuration: {}
  - id: tooBlurryAlarm
    channelTypeUID: ipcamera:tooBlurryAlarm
    label: Too Blurry Alarm
    description: Image is out of focus.
    configuration: {}
  - id: gotoPreset
    channelTypeUID: ipcamera:gotoPreset
    label: Go To Preset
    description: Move a P.T.Z camera to this ONVIF preset location.
    configuration: {}
  - id: mjpegUrl
    channelTypeUID: ipcamera:mjpegUrl
    label: MJPEG URL
    description: A link you can use in openHAB/HABpanel to fetch a MJPEG video feed
      from the camera.
    configuration: {}
  - id: rtspUrl
    channelTypeUID: ipcamera:rtspUrl
    label: RTSP URL
    description: A link that the camera uses for RTSP.
    configuration: {}
  - id: imageUrl
    channelTypeUID: ipcamera:imageUrl
    label: Image URL
    description: A link you can use to fetch a static image from the camera.
    configuration: {}
  - id: hlsUrl
    channelTypeUID: ipcamera:hlsUrl
    label: HLS URL
    description: A link you can use in openHAB to cast video feeds.
    configuration: {}

Just give that config the http based url that gives you mjpeg and the binding will not use ffmpeg. I just updated the binding to support rtsp urls in that config, however all rtsp based urls will fall back to using ffmpeg and this is only in snapshot builds until the next milestone is released.

1 Like

ok, thanks!

Hi @matt1,
First of, thank you for all the work you put into the binding & widgets!! Just a question, everything is working as it should, but for some reason, when i start from the first video, i can go back in history and view the recorded videos, but only up and till 49. If I try to view a video with the folder counyer at 50 and above(51,52,53) it fails. Openhab logs doesn’t give any feedback when this happens. Any ideas?

The binding limits it to stop the channel from using unlimited memory or hitting another limit. I think its a good idea to make this limit configurable so you can change it to a higher value. Did u want this or just wanted to know the reason why?

I’m guessing most people do not use this or know about this ability so keeping it to a low default value so the ram usage is low is what I think is best then you can up the value of you want 50+ history stored.

For me 50 is enough as I reduce false positives so that 50 visitors to my front door is more then I need.

Hi Matt, thank you for clearing that up, at least now I know why its doing it.
I guess there won’t be a simple way to go around this?
I was hoping to use the widget, kinda like a NVR if you might, to go back in history for a week or so to view videos. Is there maybe another way to do this, to access the saved clips on the drive?

You can specify the output folder for the video on the thing configuration.
so you can access the videos in this folder
Greets

Hi, I have successfully implemented the widget, but is there a limit that restricts the use of this widget?
I wanted to put all my 7 cameras on one page, but a maximum of 4 load and even if I separate it and split it into several pages, at least one remains black, unfortunately I don’t see any information in the logs…
I would be grateful for any information!

Edit: I run openhabian 64bit on a new installation on a pi5 with 8 GB. If I use every camera for its own the widget works.

I believe it is a limitation that is built into browsers. You can try using a device name instead of an ip for some of the cameras and that should be a work around. If the device name is openhab replace the IP with openhab for some of the camera streams. Also Google to see if the browser can have this changed. It might be called concurrent connections.

1 Like

I seems to work, I’ll run a view more tests tonight but thanks for your quick reply, I never heard of concurrent connections before!

OK after a very intense night of testing and a few frustrating drawbacks I discovered that switching to device name does solve the mjpeg issue but then I can’t play the history.
So I took advantage of your widget and changed it so it works for me:

uid: CameraSnapshotHistory_big
tags: []
props:
  parameters:
    - context: item
      label: Select the Camera (Equipment)
      name: camera
      required: true
      type: TEXT
    - description: "example: http://192.168.1.2:8080/ipcamera/CameraUniqueID/"
      label: Base URL
      name: cameraBaseURL
      required: true
      type: TEXT
    - context: item
      label: Select the Image URL Item
      name: imageItem
      required: true
      type: TEXT
    - description: Refresh rate in milliseconds (e.g., 1000 for 1 second)
      label: Refresh Rate
      name: refreshRate
      required: true
      type: INTEGER
timestamp: Jan 5, 2025, 7:31:37 PM
component: f7-card
config:
  key: "=(vars.selected === undefined) ? Math.random() : Math.random() +
    vars.selected"
  style:
    --f7-card-margin-horizontal: 0px
    --f7-card-margin-vertical: 0px
    border-radius: 6px
    height: auto
    width: 32rem
slots:
  default:
    - component: oh-video-card
      config:
        hideControls: false
        startManually: false
        url: =props.cameraBaseURL + items[props.camera +
          '_MP4_History'].state.split(",")[vars.selected] +".mp4"
        visible: =vars.archive == 2
    - component: oh-image-card
      config:
        style:
          border-radius: 6px
          height: auto
          margin: 0px
          width: 100%
        item: =props.imageItem
        refreshInterval: =props.refreshRate
        visible: =vars.archive != 2
    - component: f7-row
      config:
        class: no-gap
        style:
          position: absolute
          top: 0px
          width: 100%
          z-index: 2
      slots:
        default:
          - component: oh-button
            config:
              action: variable
              actionVariable: archive
              actionVariableValue: 2
              iconColor: red
              iconF7: folder
              iconSize: 28
              style:
                margin: -2px
              visible: "=(items[props.camera + '_MP4_History'].state !== NULL ? vars.archive
                !== 2 : false)"
              z-index: 2
          - component: Label
            config:
              style:
                color: red
                font-size: 9px
                left: 18px
                position: absolute
                top: 12px
                z-index: -1
              text: =items[props.camera+'_MP4_History_Length'].state
              visible: =vars.archive != 2 && items[props.camera+'_MP4_History_Length'].state >
                0
          - component: Label
            config:
              style:
                color: red
                font-size: 9px
                left: 12px
                position: absolute
                top: 9px
                z-index: -1
              text: =(vars.selected+1)
              visible: =vars.archive == 2
          - component: oh-button
            config:
              action: variable
              actionVariable: archive
              actionVariableValue: 1
              iconColor: red
              iconF7: videocam
              iconSize: 28
              style:
                margin: -2px
              visible: =(vars.archive == 2)
          - component: oh-button
            config:
              action: variable
              actionVariable: selected
              actionVariableValue: =(items[props.camera +'_MP4_History_Length'].state-1)
              iconColor: red
              iconF7: backward_end
              iconSize: 28
              style:
                margin: -2px
              visible: =vars.archive == 2 && items[props.camera+'_MP4_History_Length'].state >
                0
          - component: oh-button
            config:
              action: variable
              actionVariable: selected
              actionVariableValue: =(vars.selected+1 < items[props.camera
                +'_MP4_History_Length'].state)?(vars.selected+1):(items[props.camera
                +'_MP4_History_Length'].state-1)
              iconColor: red
              iconF7: arrowtriangle_left
              iconSize: 28
              style:
                margin: -2px
              visible: =vars.archive == 2 && items[props.camera+'_MP4_History_Length'].state >
                0
          - component: oh-button
            config:
              action: variable
              actionVariable: selected
              actionVariableValue: =(vars.selected != 0)?(vars.selected-1):(0)
              iconColor: red
              iconF7: arrowtriangle_right
              iconSize: 28
              style:
                margin: -2px
              visible: =vars.archive == 2 && items[props.camera+'_MP4_History_Length'].state >
                0
          - component: oh-button
            config:
              action: variable
              actionVariable: selected
              actionVariableValue: 0
              iconColor: red
              iconF7: forward_end
              iconSize: 28
              style:
                margin: -2px
              visible: =vars.archive == 2 && items[props.camera+'_MP4_History_Length'].state >
                0

Now I can use the snapshot string item from the ip-cam binding and with a refresh rate of 1000 it works like a charm for me on every device - maybe you can do your magic and rework the url-item part and release a third widget that supports snapshots.

The only thing my wife suggested and nearly drove me crazy is to get to actions if I click on the folder icon - I wanted it to switch to the video card and load the youngest video - I found one post on the forum, but I didn’t get it working. Maybe anyone can help…

So thx for your help.

I don’t know exactly what your list of recordings looks like, but assuming that the “youngest” recording is index 0 then you have several options:

  1. Just change all the places in your widget that use vars.selected to use (vars.selected || 0) instead. This means that whenever vars.selected is undefined, instead of throwing an error, expression that rely on the variable will use 0 instead. Once that is in place, you can reset the variable to undefined in the folder component at the same time that you set the archive value because the clearVariable property can be used in a component independent of the action settings:
- component: oh-button
  config:
    action: variable
    actionVariable: archive
    actionVariableValue: 2
    clearVariable:
      - selected
    iconColor: red
    iconF7: folder
    iconSize: 28
    style:
      margin: -2px
    visible: "=(items[props.camera + '_MP4_History'].state !== NULL ? vars.archive !== 2 : false)"
  1. You can change all your variables to be two different keys within one variable object. What this means is that instead of vars.selected and vars.archive you would have one variable, for example vidOpts with selected and archive keys. So you would use vars.vidOpts.selected and vars.vidOpts.archive. This helps you because with a single action you can either set all the keys in an object or just set an individual key. So, for example, when you just want to change selected you would add actionVariableKey to the action:
action: variable
actionVariable: vidOpts
actionVariableKey: selected
actionVariableValue: =whatever you want to set selected to

To change both selected and archive at the same time then when the folder button is clicked, instead of using actionVariableValue you just set the entire vidOpts variable to a new object with all the keys:

action: variable
actionVariable: vidOpts
actionVariableValue:
  selected: =whatever you want to set selected to
  archive: =whatever you want to set archive to
  1. If neither of those work, then you can try the multi-component version outlined in the post you saw. I’m not sure why what you tried didn’t work, but you can post your attempted code and I can take a look at it.

If my assumption is wrong and the youngest video is the end of the array, then 2 & 3 are still optoins, but not 1.

1 Like

Wow thx I went with option one - implemented it and tested it on my Fire Tablet - works like a charm. WAF +10 at least :wink:

I also changed the code a bit so that the folder icon is only visible if history is not 0:

uid: CameraSnapshotHistory_big
tags: []
props:
  parameters:
    - context: item
      label: Select the Camera (Equipment)
      name: camera
      required: true
      type: TEXT
    - description: "example: http://192.168.1.2:8080/ipcamera/CameraUniqueID/"
      label: Base URL
      name: cameraBaseURL
      required: true
      type: TEXT
    - context: item
      label: Select the Image URL Item
      name: imageItem
      required: true
      type: TEXT
    - description: Refresh rate in milliseconds (e.g., 1000 for 1 second)
      label: Refresh Rate
      name: refreshRate
      required: true
      type: INTEGER
timestamp: Jan 5, 2025, 11:15:36 PM
component: f7-card
config:
  key: "=((vars.selected || 0) === undefined) ? Math.random() : Math.random() +
    (vars.selected || 0)"
  style:
    --f7-card-margin-horizontal: 0px
    --f7-card-margin-vertical: 0px
    border-radius: 6px
    height: auto
    width: 32rem
slots:
  default:
    - component: oh-video-card
      config:
        hideControls: false
        startManually: false
        url: =props.cameraBaseURL + items[props.camera +
          '_MP4_History'].state.split(",")[(vars.selected || 0)] +".mp4"
        visible: =vars.archive == 2
    - component: oh-image-card
      config:
        style:
          border-radius: 6px
          height: auto
          margin: 0px
          width: 100%
        item: =props.imageItem
        refreshInterval: =props.refreshRate
        visible: =vars.archive != 2
    - component: f7-row
      config:
        class: no-gap
        style:
          position: absolute
          top: 0px
          width: 100%
          z-index: 2
      slots:
        default:
          - component: oh-button
            config:
              action: variable
              actionVariable: archive
              actionVariableValue: 2
              iconColor: red
              iconF7: folder
              iconSize: 28
              style:
                margin: -2px
              visible: "=(items[props.camera + '_MP4_History'].state !== NULL &&
                items[props.camera + '_MP4_History_Length'].state > 0 ?
                vars.archive !== 2 : false)"
              z-index: 2
          - component: Label
            config:
              style:
                color: red
                font-size: 9px
                left: 18px
                position: absolute
                top: 12px
                z-index: -1
              text: =items[props.camera+'_MP4_History_Length'].state
              visible: =vars.archive != 2 && items[props.camera+'_MP4_History_Length'].state >
                0
          - component: Label
            config:
              style:
                color: red
                font-size: 9px
                left: 12px
                position: absolute
                top: 9px
                z-index: -1
              text: =((vars.selected || 0)+1)
              visible: =vars.archive == 2
          - component: oh-button
            config:
              action: variable
              actionVariable: archive
              actionVariableValue: 1
              iconColor: red
              iconF7: videocam
              iconSize: 28
              style:
                margin: -2px
              visible: =(vars.archive == 2)
          - component: oh-button
            config:
              action: variable
              actionVariable: selected
              actionVariableValue: =(items[props.camera +'_MP4_History_Length'].state-1)
              iconColor: red
              iconF7: backward_end
              iconSize: 28
              style:
                margin: -2px
              visible: =vars.archive == 2 && items[props.camera+'_MP4_History_Length'].state >
                0
          - component: oh-button
            config:
              action: variable
              actionVariable: selected
              actionVariableValue: =((vars.selected || 0)+1 < items[props.camera
                +'_MP4_History_Length'].state)?((vars.selected || 0)+1):(items[props.camera
                +'_MP4_History_Length'].state-1)
              iconColor: red
              iconF7: arrowtriangle_left
              iconSize: 28
              style:
                margin: -2px
              visible: =vars.archive == 2 && items[props.camera+'_MP4_History_Length'].state >
                0
          - component: oh-button
            config:
              action: variable
              actionVariable: selected
              actionVariableValue: =((vars.selected || 0) != 0)?((vars.selected || 0)-1):(0)
              iconColor: red
              iconF7: arrowtriangle_right
              iconSize: 28
              style:
                margin: -2px
              visible: =vars.archive == 2 && items[props.camera+'_MP4_History_Length'].state >
                0
          - component: oh-button
            config:
              action: variable
              actionVariable: selected
              actionVariableValue: 0
              iconColor: red
              iconF7: forward_end
              iconSize: 28
              style:
                margin: -2px
              visible: =vars.archive == 2 && items[props.camera+'_MP4_History_Length'].state >
                0

Edit:
Just to be precise I also deleted the “Clear History” Button, cause for me it’s ok to have a history of 50 and I don’t like to have it cleared accidentally. If you need it just copy it from the original widget.

@JustinG thanks for posting those helpful tips.
@Anpro I agree with your changes so I made the following changes to the widget…

  1. The default value is 0 when you goto the recordings for the first time. I was trying to work out how to do that for a while then gave up.
  2. The clear button now only shows up if your logged in as an administrator. This way kids and non tech savy people can not press it by mistake.
  3. Folder icon only shows up if there are recordings.

Its also on my to do list to increase the limit of 50 recordings either to a larger value or making it a user config settings per camera.

I appreciate that, thank you.
I’m looking forward to expanding the camera history, but I think it would make sense to be able to switch through the history a little differently, perhaps the inner arrows a single step, as it is now, and the outer 10-step steps? Or maybe there’s a better idea…

I made another adjustment to the code - since I sometimes create videos that are over 20 seconds long, I also created a record circle that is displayed when a recording is in progress:

- component: f7-badge
            config:
              color: red
              style:
                border-radius: 50%
                height: 20px
                left: 50%
                position: absolute
                top: 5px
                transform: translateX(-50%);
                width: 12px
                z-index: 3
              visible: =(items[props.camera + '_MP4_Recording'].state !== NULL &&
                items[props.camera + '_MP4_Recording'].state > 0 && vars.archive
                !== 2)

You could use the double arrow << or >> normally used for fast forward to jump in larger increments, single arrows < or > and the next track buttons as they are now. However I’m not keen on having that many buttons myself especially when you consider how the video may look on smaller screens like phones. Having buttons larger enough to press reliably without fat fingers pushing the wrong button or covering up the video stream is a balance that people will need to play with.

I like to review all visitors to the front door and then clear the counter so each person does not get missed by not knowing where I was up to with my review of the recordings.

It seems like you have a good grasp on modifying the widget, so feel free to play and make suggestions. The advanced settings can be a place to enable and disable extra buttons and features.

I’m trying to make all the camera widgets have similar settings and features so that you can swap the widgets out quicker by editing the code of a page.

@Anpro I just uploaded a newer version of this widget that uses changes I have made to this newer widget from here: