Merge branch 'dev-check-cpu' into dev
Some checks failed
Build Worker Base and Application Images / check-base-changes (push) Successful in 8s
Build Worker Base and Application Images / build-base (push) Failing after 3m15s
Build Worker Base and Application Images / build-docker (push) Has been skipped
Build Worker Base and Application Images / deploy-stack (push) Has been skipped
Some checks failed
Build Worker Base and Application Images / check-base-changes (push) Successful in 8s
Build Worker Base and Application Images / build-base (push) Failing after 3m15s
Build Worker Base and Application Images / build-docker (push) Has been skipped
Build Worker Base and Application Images / deploy-stack (push) Has been skipped
This commit is contained in:
commit
0fc86fb72b
6 changed files with 778 additions and 23 deletions
|
@ -166,28 +166,123 @@ class RTSPReader:
|
|||
logger.info(f"RTSP reader thread ended for camera {self.camera_id}")
|
||||
|
||||
def _initialize_capture(self) -> bool:
|
||||
"""Initialize video capture with optimized settings for 1280x720@6fps."""
|
||||
"""Initialize video capture with hardware acceleration (NVDEC) for 1280x720@6fps."""
|
||||
try:
|
||||
# Release previous capture if exists
|
||||
if self.cap:
|
||||
self.cap.release()
|
||||
time.sleep(0.5)
|
||||
|
||||
logger.info(f"Initializing capture for camera {self.camera_id}")
|
||||
logger.info(f"Initializing capture for camera {self.camera_id} with hardware acceleration")
|
||||
hw_accel_success = False
|
||||
|
||||
# Create capture with FFMPEG backend and TCP transport for reliability
|
||||
# Use TCP instead of UDP to prevent packet loss
|
||||
rtsp_url_tcp = self.rtsp_url.replace('rtsp://', 'rtsp://')
|
||||
if '?' in rtsp_url_tcp:
|
||||
rtsp_url_tcp += '&tcp'
|
||||
else:
|
||||
rtsp_url_tcp += '?tcp'
|
||||
# Method 1: Try GStreamer with NVDEC (most efficient on NVIDIA GPUs)
|
||||
if not hw_accel_success:
|
||||
try:
|
||||
# Build GStreamer pipeline for NVIDIA hardware decoding
|
||||
gst_pipeline = (
|
||||
f"rtspsrc location={self.rtsp_url} protocols=tcp latency=100 ! "
|
||||
"rtph264depay ! h264parse ! "
|
||||
"nvv4l2decoder ! " # NVIDIA hardware decoder
|
||||
"nvvideoconvert ! " # NVIDIA hardware color conversion
|
||||
"video/x-raw,format=BGRx,width=1280,height=720 ! "
|
||||
"videoconvert ! "
|
||||
"video/x-raw,format=BGR ! "
|
||||
"appsink max-buffers=1 drop=true sync=false"
|
||||
)
|
||||
logger.info(f"Attempting GStreamer NVDEC pipeline for camera {self.camera_id}")
|
||||
self.cap = cv2.VideoCapture(gst_pipeline, cv2.CAP_GSTREAMER)
|
||||
|
||||
# Alternative: Set environment variable for RTSP transport
|
||||
import os
|
||||
os.environ['OPENCV_FFMPEG_CAPTURE_OPTIONS'] = 'rtsp_transport;tcp'
|
||||
if self.cap.isOpened():
|
||||
hw_accel_success = True
|
||||
logger.info(f"Camera {self.camera_id}: Successfully using GStreamer with NVDEC hardware acceleration")
|
||||
except Exception as e:
|
||||
logger.debug(f"Camera {self.camera_id}: GStreamer NVDEC not available: {e}")
|
||||
|
||||
self.cap = cv2.VideoCapture(self.rtsp_url, cv2.CAP_FFMPEG)
|
||||
# Method 2: Try OpenCV CUDA VideoReader (if built with CUVID support)
|
||||
if not hw_accel_success:
|
||||
try:
|
||||
# Check if OpenCV was built with CUDA codec support
|
||||
build_info = cv2.getBuildInformation()
|
||||
if 'cudacodec' in build_info or 'CUVID' in build_info:
|
||||
logger.info(f"Attempting OpenCV CUDA VideoReader for camera {self.camera_id}")
|
||||
|
||||
# Use OpenCV's CUDA backend
|
||||
self.cap = cv2.VideoCapture(self.rtsp_url, cv2.CAP_FFMPEG, [
|
||||
cv2.CAP_PROP_HW_ACCELERATION, cv2.VIDEO_ACCELERATION_ANY
|
||||
])
|
||||
|
||||
if self.cap.isOpened():
|
||||
hw_accel_success = True
|
||||
logger.info(f"Camera {self.camera_id}: Using OpenCV CUDA hardware acceleration")
|
||||
else:
|
||||
logger.debug(f"Camera {self.camera_id}: OpenCV not built with CUDA codec support")
|
||||
except Exception as e:
|
||||
logger.debug(f"Camera {self.camera_id}: OpenCV CUDA not available: {e}")
|
||||
|
||||
# Method 3: Try FFMPEG with optimal hardware acceleration (CUVID/VAAPI)
|
||||
if not hw_accel_success:
|
||||
try:
|
||||
from core.utils.ffmpeg_detector import get_optimal_rtsp_options
|
||||
import os
|
||||
|
||||
# Get optimal FFmpeg options based on detected capabilities
|
||||
optimal_options = get_optimal_rtsp_options(self.rtsp_url)
|
||||
os.environ['OPENCV_FFMPEG_CAPTURE_OPTIONS'] = optimal_options
|
||||
|
||||
logger.info(f"Attempting FFMPEG with detected hardware acceleration for camera {self.camera_id}")
|
||||
logger.debug(f"Camera {self.camera_id}: Using FFmpeg options: {optimal_options}")
|
||||
|
||||
self.cap = cv2.VideoCapture(self.rtsp_url, cv2.CAP_FFMPEG)
|
||||
|
||||
if self.cap.isOpened():
|
||||
hw_accel_success = True
|
||||
# Try to get backend info to confirm hardware acceleration
|
||||
backend = self.cap.getBackendName()
|
||||
logger.info(f"Camera {self.camera_id}: Using FFMPEG hardware acceleration (backend: {backend})")
|
||||
except Exception as e:
|
||||
logger.debug(f"Camera {self.camera_id}: FFMPEG hardware acceleration not available: {e}")
|
||||
|
||||
# Fallback to basic CUVID
|
||||
try:
|
||||
import os
|
||||
os.environ['OPENCV_FFMPEG_CAPTURE_OPTIONS'] = 'video_codec;h264_cuvid|rtsp_transport;tcp|hwaccel;cuda'
|
||||
self.cap = cv2.VideoCapture(self.rtsp_url, cv2.CAP_FFMPEG)
|
||||
|
||||
if self.cap.isOpened():
|
||||
hw_accel_success = True
|
||||
logger.info(f"Camera {self.camera_id}: Using basic FFMPEG CUVID hardware acceleration")
|
||||
except Exception as e2:
|
||||
logger.debug(f"Camera {self.camera_id}: Basic CUVID also failed: {e2}")
|
||||
|
||||
# Method 4: Try VAAPI hardware acceleration (for Intel/AMD GPUs)
|
||||
if not hw_accel_success:
|
||||
try:
|
||||
gst_pipeline = (
|
||||
f"rtspsrc location={self.rtsp_url} protocols=tcp latency=100 ! "
|
||||
"rtph264depay ! h264parse ! "
|
||||
"vaapih264dec ! " # VAAPI hardware decoder
|
||||
"vaapipostproc ! "
|
||||
"video/x-raw,format=BGRx,width=1280,height=720 ! "
|
||||
"videoconvert ! "
|
||||
"video/x-raw,format=BGR ! "
|
||||
"appsink max-buffers=1 drop=true sync=false"
|
||||
)
|
||||
logger.info(f"Attempting GStreamer VAAPI pipeline for camera {self.camera_id}")
|
||||
self.cap = cv2.VideoCapture(gst_pipeline, cv2.CAP_GSTREAMER)
|
||||
|
||||
if self.cap.isOpened():
|
||||
hw_accel_success = True
|
||||
logger.info(f"Camera {self.camera_id}: Successfully using GStreamer with VAAPI hardware acceleration")
|
||||
except Exception as e:
|
||||
logger.debug(f"Camera {self.camera_id}: GStreamer VAAPI not available: {e}")
|
||||
|
||||
# Fallback: Standard FFMPEG with software decoding
|
||||
if not hw_accel_success:
|
||||
logger.warning(f"Camera {self.camera_id}: Hardware acceleration not available, falling back to software decoding")
|
||||
import os
|
||||
os.environ['OPENCV_FFMPEG_CAPTURE_OPTIONS'] = 'rtsp_transport;tcp'
|
||||
self.cap = cv2.VideoCapture(self.rtsp_url, cv2.CAP_FFMPEG)
|
||||
|
||||
if not self.cap.isOpened():
|
||||
logger.error(f"Failed to open stream for camera {self.camera_id}")
|
||||
|
|
214
core/utils/ffmpeg_detector.py
Normal file
214
core/utils/ffmpeg_detector.py
Normal file
|
@ -0,0 +1,214 @@
|
|||
"""
|
||||
FFmpeg hardware acceleration detection and configuration
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import logging
|
||||
import re
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
logger = logging.getLogger("detector_worker")
|
||||
|
||||
|
||||
class FFmpegCapabilities:
|
||||
"""Detect and configure FFmpeg hardware acceleration capabilities."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize FFmpeg capabilities detector."""
|
||||
self.hwaccels = []
|
||||
self.codecs = {}
|
||||
self.nvidia_support = False
|
||||
self.vaapi_support = False
|
||||
self.qsv_support = False
|
||||
|
||||
self._detect_capabilities()
|
||||
|
||||
def _detect_capabilities(self):
|
||||
"""Detect available hardware acceleration methods."""
|
||||
try:
|
||||
# Get hardware accelerators
|
||||
result = subprocess.run(
|
||||
['ffmpeg', '-hide_banner', '-hwaccels'],
|
||||
capture_output=True, text=True, timeout=10
|
||||
)
|
||||
if result.returncode == 0:
|
||||
self.hwaccels = [line.strip() for line in result.stdout.strip().split('\n')[1:] if line.strip()]
|
||||
logger.info(f"Available FFmpeg hardware accelerators: {', '.join(self.hwaccels)}")
|
||||
|
||||
# Check for NVIDIA support
|
||||
self.nvidia_support = any(hw in self.hwaccels for hw in ['cuda', 'cuvid', 'nvdec'])
|
||||
self.vaapi_support = 'vaapi' in self.hwaccels
|
||||
self.qsv_support = 'qsv' in self.hwaccels
|
||||
|
||||
# Get decoder information
|
||||
self._detect_decoders()
|
||||
|
||||
# Log capabilities
|
||||
if self.nvidia_support:
|
||||
logger.info("NVIDIA hardware acceleration available (CUDA/CUVID/NVDEC)")
|
||||
if self.vaapi_support:
|
||||
logger.info("VAAPI hardware acceleration available")
|
||||
if self.qsv_support:
|
||||
logger.info("Intel QuickSync hardware acceleration available")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to detect FFmpeg capabilities: {e}")
|
||||
|
||||
def _detect_decoders(self):
|
||||
"""Detect available hardware decoders."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['ffmpeg', '-hide_banner', '-decoders'],
|
||||
capture_output=True, text=True, timeout=10
|
||||
)
|
||||
if result.returncode == 0:
|
||||
# Parse decoder output to find hardware decoders
|
||||
for line in result.stdout.split('\n'):
|
||||
if 'cuvid' in line or 'nvdec' in line:
|
||||
match = re.search(r'(\w+)\s+.*?(\w+(?:_cuvid|_nvdec))', line)
|
||||
if match:
|
||||
codec_type, decoder = match.groups()
|
||||
if 'h264' in decoder:
|
||||
self.codecs['h264_hw'] = decoder
|
||||
elif 'hevc' in decoder or 'h265' in decoder:
|
||||
self.codecs['h265_hw'] = decoder
|
||||
elif 'vaapi' in line:
|
||||
match = re.search(r'(\w+)\s+.*?(\w+_vaapi)', line)
|
||||
if match:
|
||||
codec_type, decoder = match.groups()
|
||||
if 'h264' in decoder:
|
||||
self.codecs['h264_vaapi'] = decoder
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to detect decoders: {e}")
|
||||
|
||||
def get_optimal_capture_options(self, codec: str = 'h264') -> Dict[str, str]:
|
||||
"""
|
||||
Get optimal FFmpeg capture options for the given codec.
|
||||
|
||||
Args:
|
||||
codec: Video codec (h264, h265, etc.)
|
||||
|
||||
Returns:
|
||||
Dictionary of FFmpeg options
|
||||
"""
|
||||
options = {
|
||||
'rtsp_transport': 'tcp',
|
||||
'buffer_size': '1024k',
|
||||
'max_delay': '500000', # 500ms
|
||||
'fflags': '+genpts',
|
||||
'flags': '+low_delay',
|
||||
'probesize': '32',
|
||||
'analyzeduration': '0'
|
||||
}
|
||||
|
||||
# Add hardware acceleration if available
|
||||
if self.nvidia_support:
|
||||
if codec == 'h264' and 'h264_hw' in self.codecs:
|
||||
options.update({
|
||||
'hwaccel': 'cuda',
|
||||
'hwaccel_device': '0',
|
||||
'video_codec': 'h264_cuvid',
|
||||
'hwaccel_output_format': 'cuda'
|
||||
})
|
||||
logger.debug("Using NVIDIA CUVID hardware acceleration for H.264")
|
||||
elif codec == 'h265' and 'h265_hw' in self.codecs:
|
||||
options.update({
|
||||
'hwaccel': 'cuda',
|
||||
'hwaccel_device': '0',
|
||||
'video_codec': 'hevc_cuvid',
|
||||
'hwaccel_output_format': 'cuda'
|
||||
})
|
||||
logger.debug("Using NVIDIA CUVID hardware acceleration for H.265")
|
||||
|
||||
elif self.vaapi_support:
|
||||
if codec == 'h264':
|
||||
options.update({
|
||||
'hwaccel': 'vaapi',
|
||||
'hwaccel_device': '/dev/dri/renderD128',
|
||||
'video_codec': 'h264_vaapi'
|
||||
})
|
||||
logger.debug("Using VAAPI hardware acceleration")
|
||||
|
||||
return options
|
||||
|
||||
def format_opencv_options(self, options: Dict[str, str]) -> str:
|
||||
"""
|
||||
Format options for OpenCV FFmpeg backend.
|
||||
|
||||
Args:
|
||||
options: Dictionary of FFmpeg options
|
||||
|
||||
Returns:
|
||||
Formatted options string for OpenCV
|
||||
"""
|
||||
return '|'.join(f"{key};{value}" for key, value in options.items())
|
||||
|
||||
def get_hardware_encoder_options(self, codec: str = 'h264', quality: str = 'fast') -> Dict[str, str]:
|
||||
"""
|
||||
Get optimal hardware encoding options.
|
||||
|
||||
Args:
|
||||
codec: Video codec for encoding
|
||||
quality: Quality preset (fast, medium, slow)
|
||||
|
||||
Returns:
|
||||
Dictionary of encoding options
|
||||
"""
|
||||
options = {}
|
||||
|
||||
if self.nvidia_support:
|
||||
if codec == 'h264':
|
||||
options.update({
|
||||
'video_codec': 'h264_nvenc',
|
||||
'preset': quality,
|
||||
'tune': 'zerolatency',
|
||||
'gpu': '0',
|
||||
'rc': 'cbr_hq',
|
||||
'surfaces': '64'
|
||||
})
|
||||
elif codec == 'h265':
|
||||
options.update({
|
||||
'video_codec': 'hevc_nvenc',
|
||||
'preset': quality,
|
||||
'tune': 'zerolatency',
|
||||
'gpu': '0'
|
||||
})
|
||||
|
||||
elif self.vaapi_support:
|
||||
if codec == 'h264':
|
||||
options.update({
|
||||
'video_codec': 'h264_vaapi',
|
||||
'vaapi_device': '/dev/dri/renderD128'
|
||||
})
|
||||
|
||||
return options
|
||||
|
||||
|
||||
# Global instance
|
||||
_ffmpeg_caps = None
|
||||
|
||||
def get_ffmpeg_capabilities() -> FFmpegCapabilities:
|
||||
"""Get or create the global FFmpeg capabilities instance."""
|
||||
global _ffmpeg_caps
|
||||
if _ffmpeg_caps is None:
|
||||
_ffmpeg_caps = FFmpegCapabilities()
|
||||
return _ffmpeg_caps
|
||||
|
||||
def get_optimal_rtsp_options(rtsp_url: str) -> str:
|
||||
"""
|
||||
Get optimal OpenCV FFmpeg options for RTSP streaming.
|
||||
|
||||
Args:
|
||||
rtsp_url: RTSP stream URL
|
||||
|
||||
Returns:
|
||||
Formatted options string for cv2.VideoCapture
|
||||
"""
|
||||
caps = get_ffmpeg_capabilities()
|
||||
|
||||
# Detect codec from URL or assume H.264
|
||||
codec = 'h265' if any(x in rtsp_url.lower() for x in ['h265', 'hevc']) else 'h264'
|
||||
|
||||
options = caps.get_optimal_capture_options(codec)
|
||||
return caps.format_opencv_options(options)
|
173
core/utils/hardware_encoder.py
Normal file
173
core/utils/hardware_encoder.py
Normal file
|
@ -0,0 +1,173 @@
|
|||
"""
|
||||
Hardware-accelerated image encoding using NVIDIA NVENC or Intel QuickSync
|
||||
"""
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import logging
|
||||
from typing import Optional, Tuple
|
||||
import os
|
||||
|
||||
logger = logging.getLogger("detector_worker")
|
||||
|
||||
|
||||
class HardwareEncoder:
|
||||
"""Hardware-accelerated JPEG encoder using GPU."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize hardware encoder."""
|
||||
self.nvenc_available = False
|
||||
self.vaapi_available = False
|
||||
self.turbojpeg_available = False
|
||||
|
||||
# Check for TurboJPEG (fastest CPU-based option)
|
||||
try:
|
||||
from turbojpeg import TurboJPEG
|
||||
self.turbojpeg = TurboJPEG()
|
||||
self.turbojpeg_available = True
|
||||
logger.info("TurboJPEG accelerated encoding available")
|
||||
except ImportError:
|
||||
logger.debug("TurboJPEG not available")
|
||||
|
||||
# Check for NVIDIA NVENC support
|
||||
try:
|
||||
# Test if we can create an NVENC encoder
|
||||
test_frame = np.zeros((720, 1280, 3), dtype=np.uint8)
|
||||
fourcc = cv2.VideoWriter_fourcc(*'H264')
|
||||
test_writer = cv2.VideoWriter(
|
||||
"test.mp4",
|
||||
fourcc,
|
||||
30,
|
||||
(1280, 720),
|
||||
[cv2.CAP_PROP_HW_ACCELERATION, cv2.VIDEO_ACCELERATION_ANY]
|
||||
)
|
||||
if test_writer.isOpened():
|
||||
self.nvenc_available = True
|
||||
logger.info("NVENC hardware encoding available")
|
||||
test_writer.release()
|
||||
if os.path.exists("test.mp4"):
|
||||
os.remove("test.mp4")
|
||||
except Exception as e:
|
||||
logger.debug(f"NVENC not available: {e}")
|
||||
|
||||
def encode_jpeg(self, frame: np.ndarray, quality: int = 85) -> Optional[bytes]:
|
||||
"""
|
||||
Encode frame to JPEG using the fastest available method.
|
||||
|
||||
Args:
|
||||
frame: BGR image frame
|
||||
quality: JPEG quality (1-100)
|
||||
|
||||
Returns:
|
||||
Encoded JPEG bytes or None on failure
|
||||
"""
|
||||
try:
|
||||
# Method 1: TurboJPEG (3-5x faster than cv2.imencode)
|
||||
if self.turbojpeg_available:
|
||||
# Convert BGR to RGB for TurboJPEG
|
||||
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
encoded = self.turbojpeg.encode(rgb_frame, quality=quality)
|
||||
return encoded
|
||||
|
||||
# Method 2: Hardware-accelerated encoding via GStreamer (if available)
|
||||
if self.nvenc_available:
|
||||
return self._encode_with_nvenc(frame, quality)
|
||||
|
||||
# Fallback: Standard OpenCV encoding
|
||||
encode_params = [cv2.IMWRITE_JPEG_QUALITY, quality]
|
||||
success, encoded = cv2.imencode('.jpg', frame, encode_params)
|
||||
if success:
|
||||
return encoded.tobytes()
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to encode frame: {e}")
|
||||
return None
|
||||
|
||||
def _encode_with_nvenc(self, frame: np.ndarray, quality: int) -> Optional[bytes]:
|
||||
"""
|
||||
Encode using NVIDIA NVENC hardware encoder.
|
||||
|
||||
This is complex to implement directly, so we'll use a GStreamer pipeline
|
||||
if available.
|
||||
"""
|
||||
try:
|
||||
# Create a GStreamer pipeline for hardware encoding
|
||||
height, width = frame.shape[:2]
|
||||
gst_pipeline = (
|
||||
f"appsrc ! "
|
||||
f"video/x-raw,format=BGR,width={width},height={height},framerate=30/1 ! "
|
||||
f"videoconvert ! "
|
||||
f"nvvideoconvert ! " # GPU color conversion
|
||||
f"nvjpegenc quality={quality} ! " # Hardware JPEG encoder
|
||||
f"appsink"
|
||||
)
|
||||
|
||||
# This would require GStreamer Python bindings
|
||||
# For now, fall back to TurboJPEG or standard encoding
|
||||
logger.debug("NVENC JPEG encoding not fully implemented, using fallback")
|
||||
encode_params = [cv2.IMWRITE_JPEG_QUALITY, quality]
|
||||
success, encoded = cv2.imencode('.jpg', frame, encode_params)
|
||||
if success:
|
||||
return encoded.tobytes()
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"NVENC encoding failed: {e}")
|
||||
return None
|
||||
|
||||
def encode_batch(self, frames: list, quality: int = 85) -> list:
|
||||
"""
|
||||
Batch encode multiple frames for better GPU utilization.
|
||||
|
||||
Args:
|
||||
frames: List of BGR frames
|
||||
quality: JPEG quality
|
||||
|
||||
Returns:
|
||||
List of encoded JPEG bytes
|
||||
"""
|
||||
encoded_frames = []
|
||||
|
||||
if self.turbojpeg_available:
|
||||
# TurboJPEG can handle batch encoding efficiently
|
||||
for frame in frames:
|
||||
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
encoded = self.turbojpeg.encode(rgb_frame, quality=quality)
|
||||
encoded_frames.append(encoded)
|
||||
else:
|
||||
# Fallback to sequential encoding
|
||||
for frame in frames:
|
||||
encoded = self.encode_jpeg(frame, quality)
|
||||
encoded_frames.append(encoded)
|
||||
|
||||
return encoded_frames
|
||||
|
||||
|
||||
# Global encoder instance
|
||||
_hardware_encoder = None
|
||||
|
||||
|
||||
def get_hardware_encoder() -> HardwareEncoder:
|
||||
"""Get or create the global hardware encoder instance."""
|
||||
global _hardware_encoder
|
||||
if _hardware_encoder is None:
|
||||
_hardware_encoder = HardwareEncoder()
|
||||
return _hardware_encoder
|
||||
|
||||
|
||||
def encode_frame_hardware(frame: np.ndarray, quality: int = 85) -> Optional[bytes]:
|
||||
"""
|
||||
Convenience function to encode a frame using hardware acceleration.
|
||||
|
||||
Args:
|
||||
frame: BGR image frame
|
||||
quality: JPEG quality (1-100)
|
||||
|
||||
Returns:
|
||||
Encoded JPEG bytes or None on failure
|
||||
"""
|
||||
encoder = get_hardware_encoder()
|
||||
return encoder.encode_jpeg(frame, quality)
|
Loading…
Add table
Add a link
Reference in a new issue