#!/usr/bin/env python
import json
import os
import shutil
import sys

import pyct.build

from setuptools import find_packages, setup
from setuptools.command.develop import develop
from setuptools.command.install import install
from setuptools.command.sdist import sdist

PANEL_LITE_BUILD = 'PANEL_LITE' in os.environ


def get_setup_version(reponame):
    """
    Helper to get the current version from either git describe or the
    .version file (if available).
    """
    basepath = os.path.split(__file__)[0]
    version_file_path = os.path.join(basepath, reponame, '.version')
    try:
        from param import version
    except Exception:
        version = None
    if version is not None:
        return version.Version.setup_version(basepath, reponame, archive_commit="$Format:%h$")
    else:
        print("WARNING: param>=1.6.0 unavailable. If you are installing a package, "
              "this warning can safely be ignored. If you are creating a package or "
              "otherwise operating in a git repository, you should install param>=1.6.0.")
        return json.load(open(version_file_path, 'r'))['version_string']


def _build_paneljs():
    from bokeh.ext import build

    from panel.compiler import bundle_resources
    print("Building custom models:")
    panel_dir = os.path.join(os.path.dirname(__file__), "panel")
    build(panel_dir)
    print("Bundling custom model resources:")
    bundle_resources()
    if sys.platform != "win32":
        # npm can cause non-blocking stdout; so reset it just in case
        import fcntl
        flags = fcntl.fcntl(sys.stdout, fcntl.F_GETFL)
        fcntl.fcntl(sys.stdout, fcntl.F_SETFL, flags&~os.O_NONBLOCK)


class CustomDevelopCommand(develop):
    """Custom installation for development mode."""

    def run(self):
        if not PANEL_LITE_BUILD:
            _build_paneljs()
        develop.run(self)


class CustomInstallCommand(install):
    """Custom installation for install mode."""

    def run(self):
        if not PANEL_LITE_BUILD:
            _build_paneljs()
        install.run(self)


class CustomSdistCommand(sdist):
    """Custom installation for sdist mode."""

    def run(self):
        if not PANEL_LITE_BUILD:
            _build_paneljs()
        sdist.run(self)


_COMMANDS = {
    'develop': CustomDevelopCommand,
    'install': CustomInstallCommand,
    'sdist':   CustomSdistCommand,
}

try:
    from wheel.bdist_wheel import bdist_wheel

    class CustomBdistWheelCommand(bdist_wheel):
        """Custom bdist_wheel command to force cancelling qiskit-terra wheel
        creation."""

        def run(self):
            """Do nothing so the command intentionally fails."""
            if not PANEL_LITE_BUILD:
                _build_paneljs()
            bdist_wheel.run(self)

    _COMMANDS['bdist_wheel'] = CustomBdistWheelCommand
except Exception:
    pass

########## dependencies ##########

install_requires = [
    'bokeh >=2.4.0,<2.5.0',
    'param >=1.12.0',
    'pyviz_comms >=0.7.4',
    'markdown',
    'requests',
    'tqdm >=4.48.0',
    'pyct >=0.4.4',
    'bleach',
    'setuptools >=42',
    'typing_extensions',
]

_recommended = [
    'jupyterlab',
    'holoviews >1.14.1',
    'matplotlib',
    'pillow',
    'plotly'
]

_tests = [
    # Test dependencies
    'flake8',
    'parameterized',
    'pytest',
    'nbval',
    'flaky',
    'pytest-xdist',
    'pytest-cov',
    'pre-commit',
    'psutil',
    # Libraries tested in unit tests
    'folium',
    'ipympl',
    'scipy',
    'twine',
    'pandas >=1.3',
    'ipython >=7.0',
    'holoviews',
    'diskcache',
    'markdown-it-py',
    'ipyvuetify',
    'reacton',
    # Added lxml temporarily as installing pyechars or idom on Python 3.11
    # via pip tries to build it and fails. To be removed.
    'lxml',
    'numpy <1.24', # Avoid VTK test fail
]

_ui = [
    'playwright',
    'pytest-playwright'
]

extras_require = {
    'examples': [
        'hvplot',
        'plotly >=4.0',
        'altair',
        'streamz',
        'vega_datasets',
        'vtk ==9.0.1',
        'scikit-learn',
        'datashader',
        'jupyter_bokeh >=3.0.2',
        'django <4',
        'channels',
        'pyvista<0.33',
        'ipywidgets',
        'ipywidgets_bokeh',
        'ipyvolume',
        'ipyleaflet',
        'ipympl',
        'folium',
        'xarray',
        'pyinstrument >=4.0',
        'aiohttp',
        'croniter',
        'graphviz',
        'networkx >=2.5',
        'pygraphviz',
        'seaborn',
        'pydeck',
        'graphviz',
        'lxml',
        'python-graphviz',
        'xgboost',
        'ipyvuetify',
        'reacton'
    ],
    'tests': _tests,
    'recommended': _recommended,
    'doc': _recommended + [
        'nbsite >=0.7.2rc2',
        'pydata-sphinx-theme <=0.9.0',
        'sphinx-copybutton',
        'sphinx-design',
    ],
    'ui': _ui
}

extras_require['all'] = sorted(set(sum(extras_require.values(), [])))

# Superset of what's in pyproject.toml (includes non-python
# dependencies).  Also, pyproject.toml isn't supported by all tools
# anyway (e.g. older versions of pip, or conda - which also supports
# non-python dependencies). Note that setup_requires isn't used
# because it doesn't work well with pip.
extras_require['build'] = [
    'param >=1.9.2',
    'pyct >=0.4.4',
    'setuptools >=42',
    'bokeh >=2.4.3,<2.5.0',
    'pyviz_comms >=0.7.4',
    'requests',
    'packaging',
    'bleach',
    'tqdm >=4.48.0',
]

setup_args = dict(
    name='panel',
    version=get_setup_version("panel"),
    description='A high level app and dashboarding solution for Python.',
    long_description=open('README.md').read() if os.path.isfile('README.md') else 'Consult README.md',
    long_description_content_type="text/markdown",
    author="HoloViz",
    author_email="developers@holoviz.org",
    maintainer="HoloViz",
    maintainer_email="developers@holoviz.org",
    platforms=['Windows', 'Mac OS X', 'Linux'],
    license='BSD',
    url='http://panel.holoviz.org',
    project_urls={
        'Source': 'https://github.com/holoviz/panel',
    },
    cmdclass=_COMMANDS,
    packages=find_packages(),
    include_package_data=True,
    data_files=[
        # like `jupyter serverextension enable --sys-prefix`
        (
            "etc/jupyter/jupyter_notebook_config.d",
            ["jupyter-config/jupyter_notebook_config.d/panel-client-jupyter.json"],
        ),
        # like `jupyter server extension enable --sys-prefix`
        (
            "etc/jupyter/jupyter_server_config.d",
            ["jupyter-config/jupyter_server_config.d/panel-client-jupyter.json"],
        ),
    ],
    classifiers=[
        "License :: OSI Approved :: BSD License",
        "Development Status :: 5 - Production/Stable",
        "Programming Language :: Python :: 3",
        "Programming Language :: Python :: 3.7",
        "Programming Language :: Python :: 3.8",
        "Programming Language :: Python :: 3.9",
        "Programming Language :: Python :: 3.10",
        "Programming Language :: Python :: 3.11",
        "Operating System :: OS Independent",
        "Intended Audience :: Developers",
        "Intended Audience :: Science/Research",
        "Intended Audience :: Financial and Insurance Industry",
        "Intended Audience :: Healthcare Industry",
        "Intended Audience :: Information Technology",
        "Intended Audience :: Legal Industry",
        "Intended Audience :: Other Audience",
        "Intended Audience :: Science/Research",
        "Natural Language :: English",
        "Topic :: Scientific/Engineering",
        "Topic :: Scientific/Engineering :: Visualization",
        "Topic :: Scientific/Engineering :: Information Analysis",
        "Topic :: Office/Business",
        "Topic :: Office/Business :: Financial",
        "Topic :: Software Development :: Libraries"],
    python_requires=">=3.7",
    entry_points={
        'console_scripts': [
            'panel = panel.command:main'
        ]
    },
    install_requires=install_requires,
    extras_require=extras_require,
    tests_require=extras_require['tests']
)

def clean_js_version(version):
    version = version.replace('-', '')
    for dev in ('a', 'b', 'rc'):
        version = version.replace(dev+'.', dev)
    return version

if __name__ == "__main__":
    example_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                                'panel', 'examples')

    if 'develop' not in sys.argv and 'egg_info' not in sys.argv:
        pyct.build.examples(example_path, __file__, force=True)

    version = setup_args['version']
    if 'post' not in version:
        with open('./panel/package.json') as f:
            package_json = json.load(f)
        js_version = package_json['version']
        version = version.split('+')[0]
        if any(dev in version for dev in ('a', 'b', 'rc')) and not '-' in js_version:
            raise ValueError(f"panel.js dev versions ({js_version}) must "
                             "must separate dev suffix with a dash, e.g. "
                             "v1.0.0rc1 should be v1.0.0-rc.1.")
        if version != 'None' and version != clean_js_version(js_version):
            raise ValueError(f"panel.js version ({js_version}) does not match "
                             f"panel version ({version}). Cannot build release.")

    setup(**setup_args)

    if os.path.isdir(example_path):
        shutil.rmtree(example_path)

setup.py
#!/usr/bin/env python
import json
import os
import shutil
import sys

import pyct.build

from setuptools import find_packages, setup
from setuptools.command.develop import develop
from setuptools.command.install import install
from setuptools.command.sdist import sdist

PANEL_LITE_BUILD = 'PANEL_LITE' in os.environ


def get_setup_version(reponame):
    """
    Helper to get the current version from either git describe or the
    .version file (if available).
    """
    basepath = os.path.split(__file__)[0]
    version_file_path = os.path.join(basepath, reponame, '.version')
    try:
        from param import version
    except Exception:
        version = None
    if version is not None:
        return version.Version.setup_version(basepath, reponame, archive_commit="$Format:%h$")
    else:
        print("WARNING: param>=1.6.0 unavailable. If you are installing a package, "
              "this warning can safely be ignored. If you are creating a package or "
              "otherwise operating in a git repository, you should install param>=1.6.0.")
        return json.load(open(version_file_path, 'r'))['version_string']


def _build_paneljs():
    from bokeh.ext import build

    from panel.compiler import bundle_resources
    print("Building custom models:")
    panel_dir = os.path.join(os.path.dirname(__file__), "panel")
    build(panel_dir)
    print("Bundling custom model resources:")
    bundle_resources()
    if sys.platform != "win32":
        # npm can cause non-blocking stdout; so reset it just in case
        import fcntl
        flags = fcntl.fcntl(sys.stdout, fcntl.F_GETFL)
        fcntl.fcntl(sys.stdout, fcntl.F_SETFL, flags&~os.O_NONBLOCK)


class CustomDevelopCommand(develop):
    """Custom installation for development mode."""

    def run(self):
        if not PANEL_LITE_BUILD:
            _build_paneljs()
        develop.run(self)


class CustomInstallCommand(install):
    """Custom installation for install mode."""

    def run(self):
        if not PANEL_LITE_BUILD:
            _build_paneljs()
        install.run(self)


class CustomSdistCommand(sdist):
    """Custom installation for sdist mode."""

    def run(self):
        if not PANEL_LITE_BUILD:
            _build_paneljs()
        sdist.run(self)


_COMMANDS = {
    'develop': CustomDevelopCommand,
    'install': CustomInstallCommand,
    'sdist':   CustomSdistCommand,
}

try:
    from wheel.bdist_wheel import bdist_wheel

    class CustomBdistWheelCommand(bdist_wheel):
        """Custom bdist_wheel command to force cancelling qiskit-terra wheel
        creation."""

        def run(self):
            """Do nothing so the command intentionally fails."""
            if not PANEL_LITE_BUILD:
                _build_paneljs()
            bdist_wheel.run(self)

    _COMMANDS['bdist_wheel'] = CustomBdistWheelCommand
except Exception:
    pass

########## dependencies ##########

install_requires = [
    'bokeh >=2.4.0,<2.5.0',
    'param >=1.12.0',
    'pyviz_comms >=0.7.4',
    'markdown',
    'requests',
    'tqdm >=4.48.0',
    'pyct >=0.4.4',
    'bleach',
    'setuptools >=42',
    'typing_extensions',
]

_recommended = [
    'jupyterlab',
    'holoviews >1.14.1',
    'matplotlib',
    'pillow',
    'plotly'
]

_tests = [
    # Test dependencies
    'flake8',
    'parameterized',
    'pytest',
    'nbval',
    'flaky',
    'pytest-xdist',
    'pytest-cov',
    'pre-commit',
    'psutil',
    # Libraries tested in unit tests
    'folium',
    'ipympl',
    'scipy',
    'twine',
    'pandas >=1.3',
    'ipython >=7.0',
    'holoviews',
    'diskcache',
    'markdown-it-py',
    'ipyvuetify',
    'reacton',
    # Added lxml temporarily as installing pyechars or idom on Python 3.11
    # via pip tries to build it and fails. To be removed.
    'lxml',
    'numpy <1.24', # Avoid VTK test fail
]

_ui = [
    'playwright',
    'pytest-playwright'
]

extras_require = {
    'examples': [
        'hvplot',
        'plotly >=4.0',
        'altair',
        'streamz',
        'vega_datasets',
        'vtk ==9.0.1',
        'scikit-learn',
        'datashader',
        'jupyter_bokeh >=3.0.2',
        'django <4',
        'channels',
        'pyvista<0.33',
        'ipywidgets',
        'ipywidgets_bokeh',
        'ipyvolume',
        'ipyleaflet',
        'ipympl',
        'folium',
        'xarray',
        'pyinstrument >=4.0',
        'aiohttp',
        'croniter',
        'graphviz',
        'networkx >=2.5',
        'pygraphviz',
        'seaborn',
        'pydeck',
        'graphviz',
        'lxml',
        'python-graphviz',
        'xgboost',
        'ipyvuetify',
        'reacton',
        'scikit-image',
    ],
    'tests': _tests,
    'recommended': _recommended,
    'doc': _recommended + [
        'nbsite >=0.7.2rc2',
        'pydata-sphinx-theme <=0.9.0',
        'sphinx-copybutton',
        'sphinx-design',
    ],
    'ui': _ui
}

extras_require['all'] = sorted(set(sum(extras_require.values(), [])))

# Superset of what's in pyproject.toml (includes non-python
# dependencies).  Also, pyproject.toml isn't supported by all tools
# anyway (e.g. older versions of pip, or conda - which also supports
# non-python dependencies). Note that setup_requires isn't used
# because it doesn't work well with pip.
extras_require['build'] = [
    'param >=1.9.2',
    'pyct >=0.4.4',
    'setuptools >=42',
    'bokeh >=2.4.3,<2.5.0',
    'pyviz_comms >=0.7.4',
    'requests',
    'packaging',
    'bleach',
    'tqdm >=4.48.0',
]

setup_args = dict(
    name='panel',
    version=get_setup_version("panel"),
    description='A high level app and dashboarding solution for Python.',
    long_description=open('README.md').read() if os.path.isfile('README.md') else 'Consult README.md',
    long_description_content_type="text/markdown",
    author="HoloViz",
    author_email="developers@holoviz.org",
    maintainer="HoloViz",
    maintainer_email="developers@holoviz.org",
    platforms=['Windows', 'Mac OS X', 'Linux'],
    license='BSD',
    url='http://panel.holoviz.org',
    project_urls={
        'Source': 'https://github.com/holoviz/panel',
    },
    cmdclass=_COMMANDS,
    packages=find_packages(),
    include_package_data=True,
    data_files=[
        # like `jupyter serverextension enable --sys-prefix`
        (
            "etc/jupyter/jupyter_notebook_config.d",
            ["jupyter-config/jupyter_notebook_config.d/panel-client-jupyter.json"],
        ),
        # like `jupyter server extension enable --sys-prefix`
        (
            "etc/jupyter/jupyter_server_config.d",
            ["jupyter-config/jupyter_server_config.d/panel-client-jupyter.json"],
        ),
    ],
    classifiers=[
        "License :: OSI Approved :: BSD License",
        "Development Status :: 5 - Production/Stable",
        "Programming Language :: Python :: 3",
        "Programming Language :: Python :: 3.7",
        "Programming Language :: Python :: 3.8",
        "Programming Language :: Python :: 3.9",
        "Programming Language :: Python :: 3.10",
        "Programming Language :: Python :: 3.11",
        "Operating System :: OS Independent",
        "Intended Audience :: Developers",
        "Intended Audience :: Science/Research",
        "Intended Audience :: Financial and Insurance Industry",
        "Intended Audience :: Healthcare Industry",
        "Intended Audience :: Information Technology",
        "Intended Audience :: Legal Industry",
        "Intended Audience :: Other Audience",
        "Intended Audience :: Science/Research",
        "Natural Language :: English",
        "Topic :: Scientific/Engineering",
        "Topic :: Scientific/Engineering :: Visualization",
        "Topic :: Scientific/Engineering :: Information Analysis",
        "Topic :: Office/Business",
        "Topic :: Office/Business :: Financial",
        "Topic :: Software Development :: Libraries"],
    python_requires=">=3.7",
    entry_points={
        'console_scripts': [
            'panel = panel.command:main'
        ]
    },
    install_requires=install_requires,
    extras_require=extras_require,
    tests_require=extras_require['tests']
)

def clean_js_version(version):
    version = version.replace('-', '')
    for dev in ('a', 'b', 'rc'):
        version = version.replace(dev+'.', dev)
    return version

if __name__ == "__main__":
    example_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                                'panel', 'examples')

    if 'develop' not in sys.argv and 'egg_info' not in sys.argv:
        pyct.build.examples(example_path, __file__, force=True)

    version = setup_args['version']
    if 'post' not in version:
        with open('./panel/package.json') as f:
            package_json = json.load(f)
        js_version = package_json['version']
        version = version.split('+')[0]
        if any(dev in version for dev in ('a', 'b', 'rc')) and not '-' in js_version:
            raise ValueError(f"panel.js dev versions ({js_version}) must "
                             "must separate dev suffix with a dash, e.g. "
                             "v1.0.0rc1 should be v1.0.0-rc.1.")
        if version != 'None' and version != clean_js_version(js_version):
            raise ValueError(f"panel.js version ({js_version}) does not match "
                             f"panel version ({version}). Cannot build release.")

    setup(**setup_args)

    if os.path.isdir(example_path):
        shutil.rmtree(example_path)

setup.py
diff --git a/.gitignore b/.gitignore
index 37794ba72f..d176cfd4f9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -126,3 +126,8 @@ discover/
 node_modules/*
 /tmp
 .devcontainer/*
+pyodide/
+script.*
+doc/_build/*
+doc/gallery/*
+doc/reference/*
diff --git a/examples/assets/VideoStreamInterface.jpg b/examples/assets/VideoStreamInterface.jpg
new file mode 100644
index 0000000000..d0216f4b16
Binary files /dev/null and b/examples/assets/VideoStreamInterface.jpg differ
diff --git a/examples/assets/VideoStreamInterfaceFaceDetectionViewer.jpg b/examples/assets/VideoStreamInterfaceFaceDetectionViewer.jpg
new file mode 100644
index 0000000000..9d1da7b707
Binary files /dev/null and b/examples/assets/VideoStreamInterfaceFaceDetectionViewer.jpg differ
diff --git a/examples/assets/VideoStreamInterfaceSobel.jpg b/examples/assets/VideoStreamInterfaceSobel.jpg
new file mode 100644
index 0000000000..bff72a4ddf
Binary files /dev/null and b/examples/assets/VideoStreamInterfaceSobel.jpg differ
diff --git a/examples/assets/VideoStreamInterfaceTimer.jpg b/examples/assets/VideoStreamInterfaceTimer.jpg
new file mode 100644
index 0000000000..320529dec0
Binary files /dev/null and b/examples/assets/VideoStreamInterfaceTimer.jpg differ
diff --git a/examples/gallery/streaming/streaming_videostream.ipynb b/examples/gallery/streaming/streaming_videostream.ipynb
new file mode 100644
index 0000000000..f968e1e5bf
--- /dev/null
+++ b/examples/gallery/streaming/streaming_videostream.ipynb
@@ -0,0 +1,736 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "id": "b8e61c5c-4f3e-4980-be22-4010a247babd",
+   "metadata": {},
+   "source": [
+    "## Streaming Video: Live video streams made easy\n",
+    "\n",
+    "In this example we will demonstrate how to develop a general tool to transform live image streams from the users web cam. We will be using Panels\n",
+    "[`VideoStream`](https://panel.holoviz.org/reference/widgets/VideoStream.html) widget to record and stream the images.\n",
+    "\n",
+    "![VideoStreamInterfaceSobel](../../assets/VideoStreamInterfaceSobel.jpg)\n",
+    "\n",
+    "We will also show how to apply *blur*, *grayscale*, *sobel* and *face recognition* models to the video stream."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "5a3760b6-f8a7-46ad-8831-620a131c1512",
+   "metadata": {},
+   "source": [
+    "## Imports and Settings\n",
+    "\n",
+    "Among other things we will be using [numpy](https://numpy.org/), [PIL](https://pillow.readthedocs.io/en/stable/) and [scikit-image](https://scikit-image.org/) to work with the images."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "02b862bc-f710-456b-9a34-f1b74a76df2c",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import base64\n",
+    "import io\n",
+    "import time\n",
+    "\n",
+    "import numpy as np\n",
+    "import param\n",
+    "import PIL\n",
+    "import skimage\n",
+    "\n",
+    "from PIL import Image, ImageFilter\n",
+    "from skimage import data, filters\n",
+    "from skimage.color.adapt_rgb import adapt_rgb, each_channel\n",
+    "from skimage.draw import rectangle\n",
+    "from skimage.exposure import rescale_intensity\n",
+    "from skimage.feature import Cascade\n",
+    "\n",
+    "import panel as pn\n",
+    "\n",
+    "pn.extension(sizing_mode=\"stretch_width\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "43e68289-6c33-435f-b279-cf89327266ac",
+   "metadata": {},
+   "source": [
+    "We define the *height* and *width* of the images to transform. Smaller is faster.\n",
+    "We also define the *timeout*, i.e. how often the videostream takes and streams a new image."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "7b46542f-fa1a-4dde-a8c6-f023f3358a6c",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "HEIGHT = 500 # pixels\n",
+    "WIDTH = 500 # pixels\n",
+    "TIMEOUT = 500 # miliseconds"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "bb8b1066-c799-4d6a-94d0-361dd3cc6007",
+   "metadata": {},
+   "source": [
+    "## Base Image Models\n",
+    "\n",
+    "We will need to define some *base image models* components. The base models are custom Panel components that inherit from Panels [`Viewer`](https://panel.holoviz.org/user_guide/Custom_Components.html#viewer-components) class.\n",
+    "\n",
+    "The *base models* makes it easy to later turn *image to image* algorithms into interactive UIs like the `FaceDetectionModel` shown in the image just below.\n",
+    "\n",
+    "![VideoStreamInterfaceTimer](../../assets/VideoStreamInterfaceFaceDetectionViewer.jpg)\n",
+    "\n",
+    "Please note we restrict our selves to working with `.jpg` images. The `VideoStream` widget also support `.png` images. But `.png` images are much bigger and slower to work with."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "a507eb66-b242-441c-8d89-dba504ec79a3",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class ImageModel(pn.viewable.Viewer):\n",
+    "    \"\"\"Base class for image models.\"\"\"\n",
+    "\n",
+    "    def __init__(self, **params):\n",
+    "        super().__init__(**params)\n",
+    "\n",
+    "        with param.edit_constant(self):\n",
+    "            self.name = self.__class__.name.replace(\"Model\", \"\")\n",
+    "        self.view = self.create_view()\n",
+    "\n",
+    "    def __panel__(self):\n",
+    "        return self.view\n",
+    "\n",
+    "    def apply(self, image: str, height: int = HEIGHT, width: int = WIDTH) -> str:\n",
+    "        \"\"\"Transforms a base64 encoded jpg image to a base64 encoded jpg BytesIO object\"\"\"\n",
+    "        raise NotImplementedError()\n",
+    "\n",
+    "    def create_view(self):\n",
+    "        \"\"\"Creates a view of the parameters of the transform to enable the user to configure them\"\"\"\n",
+    "        return pn.Param(self, name=self.name)\n",
+    "\n",
+    "    def transform(self, image):\n",
+    "        \"\"\"Transforms the image\"\"\"\n",
+    "        raise NotImplementedError()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "be8256eb-f2bb-4f24-84dc-8b6b47d64958",
+   "metadata": {},
+   "source": [
+    "Lets define a base model for working with **`PIL`** images"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "5ebf5560-29f6-4b3f-815e-a70010998ba1",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class PILImageModel(ImageModel):\n",
+    "    \"\"\"Base class for PIL image models\"\"\"\n",
+    "\n",
+    "    @staticmethod\n",
+    "    def to_pil_img(value: str, height=HEIGHT, width=WIDTH):\n",
+    "        \"\"\"Converts a base64 jpeg image string to a PIL.Image\"\"\"\n",
+    "        encoded_data = value.split(\",\")[1]\n",
+    "        base64_decoded = base64.b64decode(encoded_data)\n",
+    "        image = Image.open(io.BytesIO(base64_decoded))\n",
+    "        image.draft(\"RGB\", (height, width))\n",
+    "        return image\n",
+    "\n",
+    "    @staticmethod\n",
+    "    def from_pil_img(image: Image):\n",
+    "        \"\"\"Converts a PIL.Image to a base64 encoded JPG BytesIO object\"\"\"\n",
+    "        buff = io.BytesIO()\n",
+    "        image.save(buff, format=\"JPEG\")\n",
+    "        return buff\n",
+    "\n",
+    "    def apply(self, image: str, height: int = HEIGHT, width: int = WIDTH) -> io.BytesIO:\n",
+    "        pil_img = self.to_pil_img(image, height=height, width=width)\n",
+    "\n",
+    "        transformed_image = self.transform(pil_img)\n",
+    "\n",
+    "        return self.from_pil_img(transformed_image)\n",
+    "\n",
+    "    def transform(self, image: PIL.Image) -> PIL.Image:\n",
+    "        \"\"\"Transforms the PIL.Image image\"\"\"\n",
+    "        raise NotImplementedError()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "63cff6f1-f652-4413-b230-b4602e115560",
+   "metadata": {},
+   "source": [
+    "Lets define a base model for working with **`Numpy`** images."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "128c8e1e-8695-4710-a7c1-40476ced300a",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class NumpyImageModel(ImageModel):\n",
+    "    \"\"\"Base class for np.ndarray image models\"\"\"\n",
+    "\n",
+    "    @staticmethod\n",
+    "    def to_np_ndarray(image: str, height=HEIGHT, width=WIDTH) -> np.ndarray:\n",
+    "        \"\"\"Converts a base64 encoded jpeg string to a np.ndarray\"\"\"\n",
+    "        pil_img = PILImageModel.to_pil_img(image, height=height, width=width)\n",
+    "        return np.array(pil_img)\n",
+    "\n",
+    "    @staticmethod\n",
+    "    def from_np_ndarray(image: np.ndarray) -> io.BytesIO:\n",
+    "        \"\"\"Converts np.ndarray jpeg image to a jpeg BytesIO instance\"\"\"\n",
+    "        if image.dtype == np.dtype(\"float64\"):\n",
+    "            image = (image * 255).astype(np.uint8)\n",
+    "        pil_img = PIL.Image.fromarray(image)\n",
+    "        return PILImageModel.from_pil_img(pil_img)\n",
+    "\n",
+    "    def apply(self, image: str, height: int = HEIGHT, width: int = WIDTH) -> io.BytesIO:\n",
+    "        np_array = self.to_np_ndarray(image, height=height, width=width)\n",
+    "\n",
+    "        transformed_image = self.transform(np_array)\n",
+    "\n",
+    "        return self.from_np_ndarray(transformed_image)\n",
+    "\n",
+    "    def transform(self, image: np.ndarray) -> np.ndarray:\n",
+    "        \"\"\"Transforms the np.array image\"\"\"\n",
+    "        raise NotImplementedError()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "77bf1e99-701e-4dad-bd8a-faba4bdd446a",
+   "metadata": {},
+   "source": [
+    "## Timer\n",
+    "\n",
+    "Lets define a timer component to visualize the stats of the live videostream and the image transformations\n",
+    "\n",
+    "![VideoStreamInterfaceTimer](../../assets/VideoStreamInterfaceTimer.jpg)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "92a4ec91-3a6d-43be-8337-690614bb10cb",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class Timer(pn.viewable.Viewer):\n",
+    "    \"\"\"Helper Component used to show duration trends\"\"\"\n",
+    "\n",
+    "    _trends = param.Dict()\n",
+    "\n",
+    "    def __init__(self, **params):\n",
+    "        super().__init__()\n",
+    "\n",
+    "        self.last_updates = {}\n",
+    "        self._trends = {}\n",
+    "\n",
+    "        self._layout = pn.Row(**params)\n",
+    "\n",
+    "    def time_it(self, name, func, *args, **kwargs):\n",
+    "        \"\"\"Measures the duration of the execution of the func function and reports it under the\n",
+    "        name specified\"\"\"\n",
+    "        start = time.time()\n",
+    "        result = func(*args, **kwargs)\n",
+    "        end = time.time()\n",
+    "        duration = round(end - start, 2)\n",
+    "        self._report(name=name, duration=duration)\n",
+    "        return result\n",
+    "\n",
+    "    def inc_it(self, name):\n",
+    "        \"\"\"Measures the duration since the last time `inc_it` was called and reports it under the\n",
+    "        specified name\"\"\"\n",
+    "        start = self.last_updates.get(name, time.time())\n",
+    "        end = time.time()\n",
+    "        duration = round(end - start, 2)\n",
+    "        self._report(name=name, duration=duration)\n",
+    "        self.last_updates[name] = end\n",
+    "\n",
+    "    def _report(self, name, duration):\n",
+    "        if not name in self._trends:\n",
+    "            self._trends[name] = pn.indicators.Trend(\n",
+    "                title=name,\n",
+    "                data={\"x\": [1], \"y\": [duration]},\n",
+    "                height=100,\n",
+    "                width=150,\n",
+    "                sizing_mode=\"fixed\",\n",
+    "            )\n",
+    "            self.param.trigger(\"_trends\")\n",
+    "        else:\n",
+    "            trend = self._trends[name]\n",
+    "            next_x = max(trend.data[\"x\"]) + 1\n",
+    "            trend.stream({\"x\": [next_x], \"y\": [duration]}, rollover=10)\n",
+    "\n",
+    "    @pn.depends(\"_trends\")\n",
+    "    def _panel(self):\n",
+    "        self._layout[:] = list(self._trends.values())\n",
+    "        return self._layout\n",
+    "\n",
+    "    def __panel__(self):\n",
+    "        return pn.panel(self._panel)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "44851482-f580-49a7-ac53-2132d3f29bc0",
+   "metadata": {},
+   "source": [
+    "## VideoStreamInterface\n",
+    "\n",
+    "The `VideoStreamInterface` will be putting things together in a nice UI.\n",
+    "\n",
+    "![VideoStreamInterfaceSobel](../../assets/VideoStreamInterfaceSobel.jpg)\n",
+    "\n",
+    "Lets define a helper function first"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "b4de82da-81e6-4799-a1d3-a54bf8e017b1",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def to_instance(value, **params):\n",
+    "    \"\"\"Converts the value to an instance\n",
+    "\n",
+    "    Args:\n",
+    "        value: A param.Parameterized class or instance\n",
+    "\n",
+    "    Returns:\n",
+    "        An instance of the param.Parameterized class\n",
+    "    \"\"\"\n",
+    "    if isinstance(value, param.Parameterized):\n",
+    "        value.param.update(**params)\n",
+    "        return value\n",
+    "    return value(**params)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "d74ba396-7972-4711-8ff5-b4d27b31120c",
+   "metadata": {},
+   "source": [
+    "The `VideoStreamInterface` will take a list of `ImageModel`s. The user can the select and apply the models to the images from the `VideoStream`."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "c6dc71b7-34b2-4171-8a72-46ab9edeba91",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class VideoStreamInterface(pn.viewable.Viewer):\n",
+    "    \"\"\"An easy to use interface for a VideoStream and a set of transforms\"\"\"\n",
+    "\n",
+    "    video_stream = param.ClassSelector(\n",
+    "        class_=pn.widgets.VideoStream, constant=True, doc=\"The source VideoStream\"\n",
+    "    )\n",
+    "\n",
+    "    height = param.Integer(\n",
+    "        HEIGHT,\n",
+    "        bounds=(10, 2000),\n",
+    "        step=10,\n",
+    "        doc=\"\"\"The height of the image converted and shown\"\"\",\n",
+    "    )\n",
+    "    width = param.Integer(\n",
+    "        WIDTH,\n",
+    "        bounds=(10, 2000),\n",
+    "        step=10,\n",
+    "        doc=\"\"\"The width of the image converted and shown\"\"\",\n",
+    "    )\n",
+    "\n",
+    "    model = param.Selector(doc=\"The currently selected model\")\n",
+    "\n",
+    "    def __init__(\n",
+    "        self,\n",
+    "        models,\n",
+    "        timeout=TIMEOUT,\n",
+    "        paused=False,\n",
+    "        **params,\n",
+    "    ):\n",
+    "        super().__init__(\n",
+    "            video_stream=pn.widgets.VideoStream(\n",
+    "                name=\"Video Stream\",\n",
+    "                timeout=timeout,\n",
+    "                paused=paused,\n",
+    "                height=0,\n",
+    "                width=0,\n",
+    "                visible=False,\n",
+    "                format=\"jpeg\",\n",
+    "            ),\n",
+    "            **params,\n",
+    "        )\n",
+    "        self.image = pn.pane.JPG(\n",
+    "            height=self.height, width=self.width, sizing_mode=\"fixed\"\n",
+    "        )\n",
+    "        self._updating = False\n",
+    "        models = [to_instance(model) for model in models]\n",
+    "        self.param.model.objects = models\n",
+    "        self.model = models[0]\n",
+    "        self.timer = Timer(sizing_mode=\"stretch_width\")\n",
+    "        self.settings = self._create_settings()\n",
+    "        self._panel = self._create_panel()\n",
+    "\n",
+    "    def _create_settings(self):\n",
+    "        return pn.Column(\n",
+    "            pn.Param(\n",
+    "                self.video_stream,\n",
+    "                parameters=[\"timeout\", \"paused\"],\n",
+    "                widgets={\n",
+    "                    \"timeout\": {\n",
+    "                        \"widget_type\": pn.widgets.IntSlider,\n",
+    "                        \"start\": 10,\n",
+    "                        \"end\": 2000,\n",
+    "                        \"step\": 10,\n",
+    "                    }\n",
+    "                },\n",
+    "            ),\n",
+    "            self.timer,\n",
+    "            pn.Param(self, parameters=[\"height\", \"width\"], name=\"Image\"),\n",
+    "            pn.Param(\n",
+    "                self,\n",
+    "                parameters=[\"model\"],\n",
+    "                expand_button=False,\n",
+    "                expand=False,\n",
+    "                widgets={\n",
+    "                    \"model\": {\n",
+    "                        \"widget_type\": pn.widgets.RadioButtonGroup,\n",
+    "                        \"orientation\": \"vertical\",\n",
+    "                        \"button_type\": \"success\",\n",
+    "                    }\n",
+    "                },\n",
+    "                name=\"Model\",\n",
+    "            ),\n",
+    "            self._get_transform,\n",
+    "        )\n",
+    "\n",
+    "    def _create_panel(self):\n",
+    "        return pn.Row(\n",
+    "            self.video_stream,\n",
+    "            pn.layout.HSpacer(),\n",
+    "            self.image,\n",
+    "            pn.layout.HSpacer(),\n",
+    "            sizing_mode=\"stretch_width\",\n",
+    "            align=\"center\",\n",
+    "        )\n",
+    "\n",
+    "    @pn.depends(\"height\", \"width\", watch=True)\n",
+    "    def _update_height_width(self):\n",
+    "        self.image.height = self.height\n",
+    "        self.image.width = self.width\n",
+    "\n",
+    "    @pn.depends(\"model\")\n",
+    "    def _get_transform(self):\n",
+    "        # Hack: returning self.transform stops working after browsing the transforms for a while\n",
+    "        return self.model.view\n",
+    "\n",
+    "    def __panel__(self):\n",
+    "        return self._panel\n",
+    "\n",
+    "    @pn.depends(\"video_stream.value\", watch=True)\n",
+    "    def _handle_stream(self):\n",
+    "        if self._updating:\n",
+    "            return\n",
+    "\n",
+    "        self._updating = True\n",
+    "        if self.model and self.video_stream.value:\n",
+    "            value = self.video_stream.value\n",
+    "            try:\n",
+    "                image = self.timer.time_it(\n",
+    "                    name=\"Model\",\n",
+    "                    func=self.model.apply,\n",
+    "                    image=value,\n",
+    "                    height=self.height,\n",
+    "                    width=self.width,\n",
+    "                )\n",
+    "                self.image.object = image\n",
+    "            except PIL.UnidentifiedImageError:\n",
+    "                print(\"unidentified image\")\n",
+    "\n",
+    "            self.timer.inc_it(\"Last Update\")\n",
+    "        self._updating = False"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "9afa4c9b-a541-4a77-bb38-68d85f0a82d4",
+   "metadata": {},
+   "source": [
+    "## Custom Image Models\n",
+    "\n",
+    "We will now make specific image to image algorithms interactive.\n",
+    "\n",
+    "Let us start with the [Gaussian Blur](https://pillow.readthedocs.io/en/stable/reference/ImageFilter.html#PIL.ImageFilter.GaussianBlur) algorithm."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "f389b022-28b0-4ce2-a629-0df728a235bb",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class GaussianBlurModel(PILImageModel):\n",
+    "    \"\"\"Gaussian Blur Model\n",
+    "\n",
+    "    https://pillow.readthedocs.io/en/stable/reference/ImageFilter.html#PIL.ImageFilter.GaussianBlur\n",
+    "    \"\"\"\n",
+    "\n",
+    "    radius = param.Integer(default=2, bounds=(0, 10))\n",
+    "\n",
+    "    def transform(self, image: Image):\n",
+    "        return image.filter(ImageFilter.GaussianBlur(radius=self.radius))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "42d93547-bfce-4deb-a7e5-387cab9fac8e",
+   "metadata": {},
+   "source": [
+    "Lets implement a [Grayscale](https://scikit-image.org/docs/0.15.x/auto_examples/color_exposure/plot_rgb_to_gray.html) algorithm."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "13f7f769-af74-4027-be8d-a22208e31800",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class GrayscaleModel(NumpyImageModel):\n",
+    "    \"\"\"GrayScale Model\n",
+    "\n",
+    "    https://scikit-image.org/docs/0.15.x/auto_examples/color_exposure/plot_rgb_to_gray.html\n",
+    "    \"\"\"\n",
+    "\n",
+    "    def transform(self, image: np.ndarray):\n",
+    "        grayscale = skimage.color.rgb2gray(image[:, :, :3])\n",
+    "        return skimage.color.gray2rgb(grayscale)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "b3461593-e63d-424a-b402-cbd6fcc75ffe",
+   "metadata": {},
+   "source": [
+    "Lets implement the [Sobel](https://scikit-image.org/docs/0.15.x/auto_examples/color_exposure/plot_adapt_rgb.html) algorithm."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "0991dd1c-0557-4dfa-b940-7918b0363d6c",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class SobelModel(NumpyImageModel):\n",
+    "    \"\"\"Sobel Model\n",
+    "\n",
+    "    https://scikit-image.org/docs/0.15.x/auto_examples/color_exposure/plot_adapt_rgb.html\n",
+    "    \"\"\"\n",
+    "    def transform(self, image):\n",
+    "\n",
+    "\n",
+    "        @adapt_rgb(each_channel)\n",
+    "        def sobel_each(image):\n",
+    "            return filters.sobel(image)\n",
+    "\n",
+    "        return rescale_intensity(1 - sobel_each(image))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "cadae0c9-0191-4ca4-a770-c3be0b0987b7",
+   "metadata": {},
+   "source": [
+    "Lets implement the [face detection model](https://scikit-image.org/docs/0.15.x/auto_examples/applications/plot_face_detection.html) of scikit-image."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "57423cdc-e23e-4236-ace8-452e8068970c",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "@pn.cache()\n",
+    "def get_detector():\n",
+    "    \"\"\"Returns the Cascade detector\"\"\"\n",
+    "    trained_file = data.lbp_frontal_face_cascade_filename()\n",
+    "    return Cascade(trained_file)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "5baa7f38-6368-4ed1-883c-7a0247d1c08a",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class FaceDetectionModel(NumpyImageModel):\n",
+    "    \"\"\"Face detection using a cascade classifier.\n",
+    "\n",
+    "    https://scikit-image.org/docs/0.15.x/auto_examples/applications/plot_face_detection.html\n",
+    "    \"\"\"\n",
+    "\n",
+    "    scale_factor = param.Number(1.4, bounds=(1.0, 2.0), step=0.1)\n",
+    "    step_ratio = param.Integer(1, bounds=(1, 10))\n",
+    "    size_x = param.Range(default=(60, 322), bounds=(10, 500))\n",
+    "    size_y = param.Range(default=(60, 322), bounds=(10, 500))\n",
+    "\n",
+    "    def transform(self, image):\n",
+    "        detector = get_detector()\n",
+    "        detected = detector.detect_multi_scale(\n",
+    "            img=image,\n",
+    "            scale_factor=self.scale_factor,\n",
+    "            step_ratio=self.step_ratio,\n",
+    "            min_size=(self.size_x[0], self.size_y[0]),\n",
+    "            max_size=(self.size_x[1], self.size_y[1]),\n",
+    "        )\n",
+    "\n",
+    "        for patch in detected:\n",
+    "            rrr, ccc = rectangle(\n",
+    "                start=(patch[\"r\"], patch[\"c\"]),\n",
+    "                extent=(patch[\"height\"], patch[\"width\"]),\n",
+    "                shape=image.shape[:2],\n",
+    "            )\n",
+    "            image[rrr, ccc, 0] = 200\n",
+    "\n",
+    "        return image"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "253e15c1-0097-456f-b979-fbf42de8d270",
+   "metadata": {},
+   "source": [
+    "Please note that these models are just examples. You can also implement your own models using Scikit-Image, Pytorch, Tensorflow etc and use the `VideoStreamInterface` to work interactively with them."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "1e6ffe9c-7885-4cb8-9793-97c00862e8ba",
+   "metadata": {},
+   "source": [
+    "## Its alive!\n",
+    "\n",
+    "Lets define an instance of the `VideoStreamInterface`"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "47d8118f-77b0-4edf-a733-2b9ba877e25e",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "component = VideoStreamInterface(\n",
+    "    models=[\n",
+    "        GaussianBlurModel,\n",
+    "        GrayscaleModel,\n",
+    "        SobelModel,\n",
+    "        FaceDetectionModel,\n",
+    "    ]\n",
+    ")\n",
+    "pn.Row(pn.Row(component.settings, max_width=400), component)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "651ea40a-4d91-4a17-8761-c8b5cc446197",
+   "metadata": {},
+   "source": [
+    "## Wrap it in a template\n",
+    "\n",
+    "What makes Panel unique is that our components work very well in both the notebook and as standalone data apps.\n",
+    "\n",
+    "We can wrap the component in the nicely styled [`FastListTemplate`](https://panel.holoviz.org/reference/templates/FastListTemplate.html) to make it *ready for production*."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "dab6006d-001d-4217-ade7-ff2d0d77752f",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "pn.template.FastListTemplate(\n",
+    "    site=\"Panel\",\n",
+    "    title=\"VideoStream Interface\",\n",
+    "    sidebar=[component.settings],\n",
+    "    main=[component],\n",
+    ").servable(); # We add ; to not show the template in the notebook as it does not display well."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "9b00da3e-87fd-4e3e-bc81-e8bf827e2818",
+   "metadata": {},
+   "source": [
+    "## Serve it as a server side app"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "7191262b-f610-494f-a797-9186a496c633",
+   "metadata": {},
+   "source": [
+    "It is now possible to serve the live app via the command `panel serve streaming_videostream.ipynb`. The app is the available at http://localhost:5006/streaming_videostream.\n",
+    "\n",
+    "<img style=\"max-height:500px\" src=\"../../assets/VideoStreamInterface.jpg\"></img>"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "01fb5b28-6e49-4aaa-a0ca-2eaa301106c2",
+   "metadata": {},
+   "source": [
+    "## Serve it as a client side app\n",
+    "\n",
+    "You can also [`panel convert`](https://panel.holoviz.org/user_guide/Running_in_Webassembly.html) this app to web assembly for even better performance.\n",
+    "\n",
+    "First you will need to create a `requirements.txt` file with the following content\n",
+    "\n",
+    "```bash\n",
+    "panel\n",
+    "numpy\n",
+    "scikit-image\n",
+    "```\n",
+    "\n",
+    "Then you can\n",
+    "\n",
+    "- Run `panel convert streaming_videostream.ipynb --to pyodide-worker --out pyodide --requirements requirements.txt`\n",
+    "- Run `python3 -m http.server` to start a web server locally\n",
+    "- Open http://localhost:8000/pyodide/streaming_videostream.html to try out the app."
+   ]
+  }
+ ],
+ "metadata": {
+  "language_info": {
+   "name": "python",
+   "pygments_lexer": "ipython3"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/setup.py b/setup.py
index d7d03a1e03..e08ddc12f5 100644
--- a/setup.py
+++ b/setup.py
@@ -191,7 +191,8 @@ def run(self):
         'python-graphviz',
         'xgboost',
         'ipyvuetify',
-        'reacton'
+        'reacton',
+        'scikit-image',
     ],
     'tests': _tests,
     'recommended': _recommended,

Make it easy and useful to use the VideoStream
I've never seen any showcases of the `VideoStream` in Panel. As Panel can update relatively quickly via Tornado and websockets it should be the best data app framework for Video. And with `panel convert` even better.

But there are lot of issues

- There are no inspirational examples out there
- When you try out you run into several issues
  - The VideoStream transfers a high resolution image
  - The VideoStream transfers a .png base64 data url. It is slow to work with in Pillow and other frameworks. JPEG is much faster to load and transform
  - You need to be able to convert from base64 to PIL.Image for using Pillow and np.ndarray for using scikit-learn etc. This might be easy for experienced data vision people. But for beginners this is so hard.

To make it easier for Panel users to get started and create something useful. I propose we add an example to the Gallery that people can use for inspiration. We can use the example to identify and solve any performance issues.

The example should end up being easy to understand and use + super performant.

## Additional Context

[Yuichiro](https://twitter.com/whitphx) has done some really amazing work to make this possible in Streamlit. We should be able to find inspiration and make something at least as awesome. See https://edit.share.stlite.net/?sampleAppId=realtime_image_processing

[![image](https://user-images.githubusercontent.com/42288570/197327999-d67c824c-4303-44e1-9a06-623304116f0d.png)](https://twitter.com/whitphx/status/1583708572063772675?s=20&t=lZLbNEIl7LQpVWu1RyNEiQ)
