OH3 Widget: Building a camera widget

Here’s my first attempt at creating a camera widget, feel free to help improve it by posting code that works for you:

  • Go to Developer Tools > Widgets, create a new one, and copy-pasting the code below.
  • Save and then you can insert the widget into pages.
  • On the page after it is inserted you click ‘edit’ followed by “configure cell” to enter in the URLs you wish to display.

EDIT: Fixed the issues and this code now works well and is done with a different method to the ones posted below in post 11 of this thread.

This one lazy loads the streams and is the widget I prefer to use so it has more features then the others.

uid: ClickableCameraImage
props:
  parameters:
    - description: "example: http://192.168.1.2:54321/autofps.mjpeg"
      label: Thumbnail URL
      name: thumbnailURL
      required: true
      type: TEXT
    - description: "example: http://192.168.1.2:54321/snapshots.mjpeg"
      label: Stream URL
      name: streamURL
      required: true
      type: TEXT
    - context: item
      label: Item to Switch
      name: switchItem
      required: false
      type: TEXT
    - context: item
      label: Camera Controls
      name: camera
      required: false
      type: TEXT
  parameterGroups: []
timestamp: Dec 29, 2020, 11:56:18 AM
component: f7-card
config:
  style:
    --f7-card-margin-horizontal: 0px
    width: 16rem
    height: 9rem
slots:
  default:
    - component: oh-image-card
      config:
        lazy: true
        style:
          margin: 0px
          border-radius: 6px
          width: 100%
          height: 9rem
        url: =props.thumbnailURL
        action: photos
        actionPhotos: =[props.streamURL]
        actionPhotoBrowserConfig:
          lazy: true
          theme: dark
          type: popup
    - component: f7-card-content
    - component: oh-link
      config:
        style:
          position: absolute
          top: 0rem
          left: 0.2rem
          color: "=(items[props.switchItem].state === 'ON') ? 'cyan' : 'white'"
          opacity: "=(items[props.switchItem].state === 'ON') ? '0.4' : '0.3'"
        visible: =props.switchItem !== undefined
        iconF7: power
        iconSize: 25
        action: toggle
        actionItem: =props.switchItem
        actionCommand: ON
        actionCommandAlt: OFF
    - component: oh-link
      config:
        visible: =props.camera !== undefined
        style:
          position: absolute
          top: 0rem
          right: 0.2rem
          color: white
          opacity: 0.4
        iconF7: gear_alt
        iconSize: 25
        action: group
        actionGroupPopupItem: =props.camera
    - component: oh-link
      config:
        style:
          position: absolute
          top: 7.8rem
          right: 0.2rem
          color: white
          opacity: "=(items[props.camera + '_MotionAlarm'].state === 'ON') ? '0.5' : '0'"
        iconF7: eye
        iconSize: 18
    - component: oh-link
      config:
        style:
          position: absolute
          top: 7.7rem
          left: 0rem
          color: white
          opacity: "=(items[props.camera + '_AudioAlarm'].state === 'ON') ? '0.5' : '0'"
        iconF7: ear
        iconSize: 18


This one loads and keeps the streams open non stop which can be a problem if you have FFmpeg creating the stream and chewing CPU power. The widget above will free up the CPU when you close the popup view, this one will not. I don’t recommend you use this older version with less features, however the code is still useful to learn from.

uid: ExpandableCameraCard
props:
  parameters:
    - description: "example: http://192.168.1.2:54321/autofps.mjpeg"
      label: Thumbnail URL
      name: thumbnailURL
      required: true
      type: TEXT
    - description: "example: http://192.168.1.2:54321/snapshots.mjpeg"
      label: Stream URL
      name: streamURL
      required: true
      type: TEXT
    - context: item
      label: Select Camera to open controls for
      name: camera
      required: false
      type: TEXT
    - context: item
      label: Item for Switch
      name: switchItem
      required: false
      type: TEXT
  parameterGroups: []
timestamp: Dec 27, 2020, 4:44:10 PM
component: f7-card
config:
  class:
    - no-margin
  expandable: true
  style:
    border-radius: 6px
    height: 9.6em
    width: 17em
slots:
  default:
    - component: f7-card-content
      config:
        class: card-opened-fade-in
        style:
          color: white
          background-image: ="url('"+props.streamURL+"')"
          background-size: 100% auto
          background-position: center
          background-repeat: no-repeat
      slots:
        default:
          - component: oh-link
            config:
              class:
                - card-prevent-open
              style:
                margin: 0em 0.25em 0 -0.7em
                color: var(--f7-card-text-color)
                opacity: 0.4
                z-index: 99
              visible: =props.switchItem !== undefined
              iconF7: power
              iconSize: 22
              round: true
              action: toggle
              actionItem: =props.switchItem
              actionCommand: ON
              actionCommandAlt: OFF
          - component: oh-link
            config:
              visible: =props.camera !== undefined
              class:
                - card-prevent-open
              style:
                margin: 0 0.25em
                color: var(--f7-card-text-color)
                opacity: 0.4
                z-index: 99
              iconF7: gear_alt
              iconSize: 22
              round: true
              action: group
              actionGroupPopupItem: =props.camera
    - component: f7-card-content
      config:
        class: card-opened-fade-out
        style:
          color: white
          background-image: ="url('"+props.thumbnailURL+"')"
          background-size: 17rem 10rem
          background-repeat: no-repeat
          border-radius: 6px
      slots:
        default:
          - component: oh-link
            config:
              class:
                - card-prevent-open
              style:
                margin: 0em 0.25em 0 -0.7em
                color: "=(items[props.switchItem].state === 'ON') ? 'cyan' : 'white'"
                opacity: "=(items[props.switchItem].state === 'ON') ? '1' : '0.3'"
              visible: =props.switchItem !== undefined
              iconF7: power
              iconSize: 22
              round: true
              action: toggle
              actionItem: =props.switchItem
              actionCommand: ON
              actionCommandAlt: OFF
          - component: oh-link
            config:
              visible: =props.camera !== undefined
              class:
                - card-prevent-open
              style:
                margin: 0 0.25em
                color: var(--f7-card-text-color)
                opacity: 0.4
              iconF7: gear_alt
              iconSize: 22
              round: true
              action: group
              actionGroupPopupItem: =props.camera

There is also a widget here that can do PTZ controls as well.

OH3 Widget: Move a PTZ camera - Add-ons / UIs - openHAB Community

1 Like

Mine opens up into big blurry mess

It also looks a little different to the standard widget

image

I have been playing with the binding for the last couple of days and its awesome.

The fuzzy view is caused by clicking on it when your in the edit mode, works when you exit edit.

This different code is also close to working but I cant work out how to give the cell a background url, so its a blank button that when pressed opens up the URL stream.

uid: CameraTest
props:
  parameters:
    - description: "example: http://192.168.1.2:54321/snapshots.mjpeg"
      label: URL
      name: URL
      required: true
      type: TEXT
timestamp: Dec 13, 2020, 9:20:33 PM
component: oh-cell
slots:
  default:
    - component: oh-image
      config:
        lazy: true
        lazyFadeIn: true
        url: =props.URL
        style:
          position: absolute
          top: 0px
          left: 0px
          maxWidth: 100%
          height: auto
          z-index: 5

Yeah I got that set as default widget for model. But a little different as it automatically sets up stream

If you use create equipment from thing and use default names like blah_MJPEGURL
You can reference that in your widget if you name it item

Eg create widget


uid: Default_Camera
tags: []
props:
  parameters:
    - context: item
      description: The Camera Equipment Item
      label: Item
      name: item
      required: false
      type: TEXT
  parameterGroups: []
component: f7-block
config:
  style:
    --f7-button-text-color: var(--f7-text-color)
    --f7-button-bg-color: var(--f7-card-bg-color)
    --f7-theme-color-rgb: var(--f7-color-blue-rgb)
  class:
    - no-padding
slots:
  default:
    - component: f7-row
      config:
        class:
          - margin
      slots:
        default:
          - component: f7-col
            slots:
              default:
                - component: oh-image
                  config:
                    class:
                      - margin
                      - no-padding
                    lazy: true
                    lazyFadeIn: true
                    url: =items[props.item + "_MJPEGURL"].state

then goto model select equipment the add metadata / default standalone widget / Default_Camera

this way you will be able to put the PTZ controls in the widget

1 Like

That has been what I have been aiming towards for many months, however for a standard camera view it is better to use a URL, because many people will want to use Blue Iris, Motion, or any other number of software to process the camera with and not the binding. When it comes to PTZ or a history of recent recordings, then that is different and the binding has been written to leverage things that hopefully will come together over time.

Here’s another piece of code that is probably the closet I have come to what I want. It requires a hard coded widget for each camera on top of this. Still playing to see what the best method will be.

uid: ModalCamera
timestamp: Dec 13, 2020, 11:05:28 PM
component: f7-card
config:
  class:
    - oh-cell
slots:
  default:
    - component: oh-button
      config:
        action: popup
        actionModal: widget:SingleCamera
        style:
          color: transparent
          padding: 75px
          background-image: url(http://192.168.1.2:54321/autofps.mjpeg)
          background-size: cover
          background-repeat: no-repeat
          background-position: center
1 Like

Cool, but you have the oh-image-card for that, it’s true that you don’t have a cell widget for images.
On a layout click Add Block, Add Row, Add Column, and then add an “Image Card” widget. Configure the item for it, if it’s an Image item it will display the image, if it’s a String it will assume it to be the URL
(you can also configure a fixed URL).

Below you have the “Action” settings, you can set it to “Open photo browser”:
The other settings are [object Object] in the screenshot below because I configured them with YAML (actionPhotos is an array and actionPhotoBrowserConfig an object):

component: oh-image-card
config:
  item: WebcamSalon_MJPEGURL
  action: photos
  actionPhotos:
    - item: WebcamSalon_MJPEGURL
  actionPhotoBrowserConfig:
    theme: dark

image

If you specify multiple items in actionPhotos you’ll have a carousel and can swipe from one to the other (or use the arrows), and add captions if you like:

component: oh-image-card
config:
  item: WebcamSalon_MJPEGURL
  action: photos
  actionPhotos:
    - item: WebcamSalon_MJPEGURL
      caption: Camera 1
    - url: https://source.unsplash.com/random
      caption: Random image
  actionPhotoBrowserConfig:
    theme: dark
1 Like

@ysc
Thanks for the pointers, I had a few things go click in my head after seeing that. However I have 1 thing which I would like your advice on if it is a bug, code on the todo list, or a limitation on the way I am doing it…

The below widget fails if I try to use =props.streamURL but if I hardcode an URL in there in the format http://192.168.1.2:54321/snapshots.mjpeg it then works great. The thumbnailURL works as a props, so it is only the location of trying to use it inside the actionPhotos (array?).

uid: ClickableCamera
props:
  parameters:
    - description: "example: http://192.168.1.2:54321/autofps.mjpeg"
      label: Thumbnail URL
      name: thumbnailURL
      required: true
      type: TEXT
    - description: "example: http://192.168.1.2:54321/snapshots.mjpeg"
      label: Stream URL
      name: streamURL
      required: true
      type: TEXT
timestamp: Dec 14, 2020, 7:04:31 PM
component: oh-image-card
config:
  class:
    - oh-cell
    - no-margin
  style:
    - border-radius: 6px
    - width: 100%
    - height: auto
  url: =props.thumbnailURL
  action: photos
  actionPhotos: 
    - url: =props.streamURL
  actionPhotoBrowserConfig:
    theme: dark
    type: popup

Yes you can’t use expressions inside array members, but you can use an expression to build the array itself. So instead of:

actionPhotos:
  - url: =props.streamURL

do:

actionPhotos: =[props.streamURL]
1 Like

Thanks, that works perfect.

I actually tried many combos that were close and all failed before I posted as I didn’t know the right syntax…

actionPhotos: [=props.streamURL]
actionPhotos: [’=props.streamURL’]

Multiple Cameras can be done like this and with - html: You should be able to play back mp4 recordings, or possibly HLS video streams.

uid: cameraAll
timestamp: Dec 14, 2020, 10:59:55 PM
component: oh-image-card
config:
  class:
    - oh-cell
    - no-margin
  style:
    - border-radius: 6px
    - width: 100%
    - height: auto
  url: https://source.unsplash.com/category/technology
  action: photos
  actionPhotos:
    - html: <video loop autoplay src="https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/720/Big_Buck_Bunny_720_10s_1MB.mp4"></video>
      caption: BunnyCamera
    - url: https://source.unsplash.com/category/nature
      caption: Camera2
    - url: https://source.unsplash.com/category/street-photography
      caption: Camera3
    - url: https://source.unsplash.com/category/interiors
      caption: Camera4
  actionPhotoBrowserConfig:
    theme: dark
    type: popup

Single Camera that can have the URLs configured.

uid: ClickableCamera
props:
  parameters:
    - description: "example: http://192.168.1.2:54321/autofps.mjpeg"
      label: Thumbnail URL
      name: thumbnailURL
      required: true
      type: TEXT
    - description: "example: http://192.168.1.2:54321/snapshots.mjpeg"
      label: Stream URL
      name: streamURL
      required: true
      type: TEXT
timestamp: Dec 14, 2020, 9:59:29 PM
component: oh-image-card
config:
  class:
    - oh-cell
    - no-margin
  style:
    - border-radius: 6px
    - width: 100%
    - height: auto
  url: =props.thumbnailURL
  action: photos
  actionPhotos: =[props.streamURL]
  actionPhotoBrowserConfig:
    theme: dark
    type: popup
4 Likes

This might even be a good candidate for a oh-swiper-card with several oh-image-card as slides.
It’s better to have all images of the same dimensions in this case, or it will look weird.

component: oh-swiper-card
config:
  pagination: true
  navigation: true
  params:
    slidesPerView: 1.5
slots:
  default:
    - component: oh-image-card
      config:
        url: https://source.unsplash.com/800x600/?frontdoor
        action: photos
        actionPhotos:
          - https://source.unsplash.com/800x600/?frontdoor
    - component: oh-image-card
      config:
        url: https://source.unsplash.com/800x600/?garage
    - component: oh-image-card
      config:
        url: https://source.unsplash.com/800x600/?garden

You can also have some fun with the SwiperJS parameters like the coverflow effect -(https://swiperjs.com/api/#coverflow-effect):

component: oh-swiper-card
config:
  pagination: true
  navigation: true
  params:
    effect: coverflow
    coverFlowEffect:
      rotate: 30
      slideShadows: false

coverflow

or the cube:

component: oh-swiper-card
config:
  pagination: true
  navigation: true
  params:
    effect: cube
    cubeEffect:
      shadow: false

cube

2 Likes

Nice Effects. Is there any way to remove the frame of the image (the space between wrapper and container I guess) ?

I have not tried but my guess is it should be possible if you use CSS to change the background color. To do this see the examples above at how CSS is used after style:
If the background color matches then you don’t see it.

Right and add noBorder & noShadow too (properties of all cards).

config:
  noBorder: true
  noShadow: true
  style:
    --f7-card-bg-color: transparent

Trying to get mjpeg video streams from cameras working in Main UI…

In OH2 sitemap I used:

Video url="http://user:pwd@192.168.0.15:80/video2.mjpg" encoding="mjpeg"

I can’t get it working in Main UI.
Error message: No compatible source was found for this media.

I tried the Video card as follows…

URL:
http://user:pwd@192.168.0.15:80/video2.mjpg

Type:
mjpg mjpeg video/mjpeg video/mjpg (none of these options worked)

Is there a way to get mjpeg stream working in Main UI?

I had luck using the “Image” card with a motion jpeg.

Which video settings does your motion jpeg stream have?

  • resolution
  • frames per second

Yes if you use the ipcamera binding and the widget in this post it should work. I can only guess that there may be an issue with embedded credentials not being supported. It was removed from the http spec due to security concerns and more and more software is removing it so hence the binding is needed.

Now it works for me too with the Image card - I had to go with the IPcamera binding though.