Commit 6d6afec6 authored by vertighel's avatar vertighel
Browse files

Started working on webcam

parent 14262481
Loading
Loading
Loading
Loading
Loading
+3 −2
Original line number Diff line number Diff line
@@ -15,6 +15,7 @@ from quart import Blueprint, request, abort
from noctua import devices
from noctua.api.sequencer_instance import seq
from noctua.api.basecontext import ends, resource_registry, config_path
from noctua.utils.logger import log
from .blocks import blocks_api
from .defaults import defaults_api
from .sequencer import sequencer_api
@@ -58,10 +59,10 @@ def dynamic_import(url_path):
        
        # Improved debug print with methods list
        full_resource_path = f"{module.__name__}.{resource_class.__name__}"
        print(f"Route registered: /api{url_path:<40} {full_resource_path:<48} {str(methods):<25}")
        log.info(f"Route registered: /api{url_path:<40} {full_resource_path:<48} {str(methods):<25}")
        
    except Exception as e:
        print(f"Error loading {url_path}: {e}")
        log.warning(f"Error loading route: {url_path<40} {e}")
        
# Load all routes from ini
for section in ends.sections():
+60 −17
Original line number Diff line number Diff line
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

'''REST API for IP camera related operations'''
# System modules
import asyncio

# Custom modules
from .baseresource import BaseResource
# Third-party modules
from quart import Response, stream_with_context
import httpx

# Custom modules
from noctua.api.baseresource import BaseResource

class Pointing(BaseResource):
    """Position of the webcam."""
@@ -26,21 +28,62 @@ class Pointing(BaseResource):
        res = await self.run_blocking(action)
        return self.make_response(res)


class Snapshot(BaseResource):
    """Image from the webcam."""

    async def get(self):
        """Retrieve a raw base/64 image from the webcam."""
        """
        Retrieve a raw JPEG image from the webcam.

        Returns
        -------
        Response
            The raw JPEG binary data with appropriate headers.
        """
        
        img_bytes = await self.run_blocking(lambda: self.dev.image)
        
        if not img_bytes:
            return Response("Camera unavailable", status=503)

        return Response(img_bytes, mimetype='image/jpeg', headers={'Cache-Control': 'no-store'})


class VideoStream(BaseResource):
    """Live MJPEG video stream proxy using Async HTTPX."""

    async def get(self):
        """
        Stream the MJPEG video directly to the client.
        Acts as an asynchronous reverse proxy.

        Returns
        -------
        Response
            An asynchronous chunked streaming response.
        """
        
        img = await self.run_blocking(lambda: self.dev.image)
        # Recupera l'URL e le credenziali dal Layer 1 senza chiamare request bloccanti
        cam_url = f"{self.dev.addr}/video/mjpg.cgi"
        
        # Original logic for status codes
        code = 200
        if not img:
            code = 401 # Unauthorized/Not available
        elif getattr(self.dev, 'error', []):
            code = 501 # Not Implemented / Device Error
        # Usiamo stream_with_context per permettere a Quart di gestire la disconnessione del client
        @stream_with_context
        async def generate():
            """
            Asynchronous generator to yield video chunks.
            """
            try:
                # Apre una connessione asincrona verso la DLink
                async with httpx.AsyncClient() as client:
                    async with client.stream("GET", cam_url, timeout=10.0) as r:
                        # Legge e inoltra il flusso pezzo per pezzo
                        async for chunk in r.aiter_bytes(chunk_size=4096):
                            yield chunk
            except Exception as e:
                # Se il browser chiude la pagina o la telecamera si scollega, chiudiamo lo stream
                pass

        # Decoding binary data to string for JSON transport
        response_data = img.decode("ISO-8859-1") if img else img
        return Response(
            generate(), 
            mimetype='multipart/x-mixed-replace; boundary=--video boundary--'
        )    
+13 −9
Original line number Diff line number Diff line
@@ -302,17 +302,21 @@ resource = Settings
device = cam
depends-on = /camera/power

# ##############
# # webcam
# ##############
##############
# webcam
##############

[/webcam/snapshot]
resource = Snapshot
device = ipcam

# [/webcam/snapshot]
# resource = Snapshot
# device = ipcam
[/webcam/position]
resource = Pointing
device = ipcam

# [/webcam/position]
# resource = Pointing
# device = ipcam
[/webcam/stream]
resource = VideoStream
device = ipcam

# ##############
# # environment
+6 −5
Original line number Diff line number Diff line
@@ -119,6 +119,12 @@ node = ASCOM_REMOTE
outlet = 3

######################################

[ipcam]
module = ipcam
class = Webcam
node = IPCAM

######################################

# [tel_temp]
@@ -128,11 +134,6 @@ outlet = 3
# outlet1 = 1
# outlet2 = 2

# [ipcam]
# module = ipcam
# class = Webcam
# node = IPCAM

# [met]
# module = meteo
# class = Meteo
+53 −9
Original line number Diff line number Diff line
@@ -25,6 +25,31 @@ class DlinkDCSCamera(BaseDevice):
        self.timeout = 3
        self.error = []

    def get_stream(self, method):
        """
        Open a continuous HTTP stream to the camera.

        Parameters
        ----------
        method : str
            The CGI endpoint (e.g., 'video/mjpg').

        Returns
        -------
        requests.Response
            The raw response object with stream=True.
        """
        
        try:
            res = requests.get(f"{self.addr}/{method}.cgi", stream=True, timeout=5)
            res.raise_for_status()
            return res
        except requests.exceptions.RequestException as e:
            log.error(f"IPCAM Stream Error: {e}")
            self.error.append(str(e))
            return None

        
    @check.request_errors
    def get(self, method, params=[], text=True):
        '''Send a HTTP GET request to the device address.'''
@@ -112,19 +137,38 @@ class Webcam(DlinkDCSCamera):

    @property
    def image(self):
        '''Get IP Camera image in raw format.'''
        """
        Get IP Camera image in raw JPEG format.

        res = self.get('image/jpeg', text=False)
        Returns
        -------
        bytes or None
            The raw binary data of the JPEG image.
        """

        return res
        return self.get('image/jpeg', text=False)

    @property
    def video(self):
        '''Get IP Camera video as mjpeg stream.'''
    def video_stream(self):
        """
        Get the continuous MJPEG video stream object.

        Returns
        -------
        requests.Response
            The streaming response.
        """
        
        return self.get_stream('video/mjpg')

    # @property
    # def video(self):
    #     '''Get IP Camera video as mjpeg stream.'''
        
    #     res = self.get('video/mjpg', text=False)

    #     return res

        res = self.get('video/mjpg', text=False)

        return res
    
    @property
    def save_image(self, filename="temp.jpeg"):
Loading