feat: Add comprehensive screen casting system with Chromecast integration

- Add ScreenCastComponent: New threaded component for screen capture and streaming
- Implement FFmpeg-based cross-platform screen capture (Linux, Windows, macOS)
- Add Chromecast device discovery and streaming using pychromecast
- Create web-based screen cast interface at /screen_cast with real-time controls
- Add HTTP streaming server for Chromecast-compatible video delivery
- Integrate screen cast settings into web dashboard configuration panel
- Add --no-screen-cast command line flag (enabled by default)
- Update dependencies: ffmpeg-python>=0.2.0, pychromecast>=13.0.0

New Files:
- mbetterclient/core/screen_cast.py: Main ScreenCastComponent implementation
- mbetterclient/web_dashboard/screen_cast_routes.py: Flask API routes
- mbetterclient/web_dashboard/templates/dashboard/screen_cast.html: Web interface
- test_screen_cast_integration.py: Integration test suite

Key Features:
- Real-time device discovery and status updates
- Platform-specific audio/video input sources
- Quality settings (resolution, frame rate, bitrate)
- Network stream URL generation for Chromecast access
- Proper component lifecycle management and cleanup
- Message bus integration with WEB_ACTION support

Documentation:
- Updated README.md with screen casting features and usage
- Added comprehensive CHANGELOG.md entry for v1.2.4
- Extended DOCUMENTATION.md with complete screen casting guide

Architecture Enhancement:
- Extended from 4 to 5 threaded components
- Added ScreenCastConfig to settings with database persistence
- Enhanced web dashboard navigation with screen cast section
parent da6b6529
...@@ -2,6 +2,52 @@ ...@@ -2,6 +2,52 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [1.2.4] - 2025-08-22
### Added
- **Complete Screen Casting System**: Integrated comprehensive screen capture and Chromecast streaming functionality into MBetterC application
- **ScreenCastComponent**: New threaded component inheriting from ThreadedComponent with full lifecycle management
- **Web-Based Screen Cast Interface**: Complete web dashboard at `/screen_cast` with device discovery, streaming controls, and real-time status updates
- **Chromecast Device Discovery**: Automatic discovery and management of Chromecast devices on the local network using pychromecast library
- **Cross-Platform Screen Capture**: FFmpeg-based screen recording with platform-specific audio/video input sources:
- Linux: `:0.0+0,0` (X11) with `pulse` audio input
- Windows: `desktop` with `dshow` audio input
- macOS: `1:0` (screen) with `:0` audio input
- **HTTP Streaming Server**: Dedicated HTTP server for serving screen capture streams to Chromecast devices
- **Administrative Configuration**: Comprehensive screen cast settings panel in web dashboard configuration with:
- Video quality settings (resolution, frame rate, bitrate)
- Audio configuration options
- Network streaming parameters
- Chromecast device management
- **Real-Time Status Management**: Live status monitoring with proper button state management and streaming feedback
- **Network Stream URL Generation**: Automatically generated network-accessible streaming URLs for Chromecast compatibility
- **Command-Line Integration**: Screen casting enabled by default with `--no-screen-cast` flag for opt-out configuration
### Enhanced
- **Application Architecture**: Extended from 4 to 5 threaded components with screen casting integration
- **Message Bus Communication**: Added `WEB_ACTION` message type support for web interface screen cast commands
- **Database Configuration**: Added `ScreenCastConfig` dataclass with comprehensive settings and database persistence
- **Web Dashboard Navigation**: Added screen cast section with intuitive device discovery and control interface
- **Dependencies Management**: Added `ffmpeg-python>=0.2.0` and `pychromecast>=13.0.0` to requirements.txt
### Fixed
- **Message Bus Registration**: Proper component registration with message bus for screen cast messaging
- **Command Line Override**: Database settings no longer override `--no-screen-cast` command line argument
- **Message Type Compatibility**: Changed from non-existent `SYSTEM_COMMAND` to proper `WEB_ACTION` message type
- **Network IP Detection**: Implemented robust local IP detection for Chromecast-accessible stream URLs
- **Button State Management**: Real-time status access for proper UI button enabling/disabling
- **Chromecast Lifecycle**: Fixed streaming start/stop cycle by properly resetting Chromecast app state
- **API Compatibility**: Handled pychromecast API changes with safe attribute access patterns
### Technical Details
- **ScreenCastComponent Implementation**: Complete threaded component with HTTP server, FFmpeg capture, and Chromecast streaming
- **Web Routes Integration**: Added `screen_cast_routes.py` Flask blueprint with comprehensive API endpoints
- **Template Integration**: Added screen cast web interface templates with real-time device discovery
- **Configuration Integration**: Screen cast settings fully integrated into main configuration system
- **Cross-Platform FFmpeg Commands**: Platform-specific screen capture command generation with proper audio/video sources
- **HTTP Server Management**: Dedicated HTTP server thread for streaming with proper cleanup and lifecycle management
- **Chromecast App Management**: Proper DefaultMediaReceiver app lifecycle with start/stop/quit sequence
## [1.2.3] - 2025-08-21 ## [1.2.3] - 2025-08-21
### Added ### Added
......
This diff is collapsed.
...@@ -5,10 +5,11 @@ A cross-platform multimedia client application with video playback, web dashboar ...@@ -5,10 +5,11 @@ A cross-platform multimedia client application with video playback, web dashboar
## Features ## Features
- **PyQt Video Player**: Fullscreen video playback with dual overlay system (WebEngine and native Qt widgets) - **PyQt Video Player**: Fullscreen video playback with dual overlay system (WebEngine and native Qt widgets)
- **Screen Casting System**: Complete screen capture and Chromecast streaming with web-based controls and device discovery
- **Template Management System**: Upload, manage, and live-reload HTML overlay templates with persistent storage - **Template Management System**: Upload, manage, and live-reload HTML overlay templates with persistent storage
- **Web Dashboard**: Authentication, user management, configuration interface, and admin system controls - **Web Dashboard**: Authentication, user management, configuration interface, and admin system controls
- **REST API Client**: Configurable external API integration with automatic retry - **REST API Client**: Configurable external API integration with automatic retry
- **Multi-threaded Architecture**: Four threads with Queue-based message passing and proper daemon thread management - **Multi-threaded Architecture**: Five threads with Queue-based message passing and proper daemon thread management
- **Offline Capability**: Works seamlessly without internet connectivity - **Offline Capability**: Works seamlessly without internet connectivity
- **Cross-Platform**: Supports Windows, Linux, and macOS - **Cross-Platform**: Supports Windows, Linux, and macOS
- **Single Executable**: Built with PyInstaller for easy deployment - **Single Executable**: Built with PyInstaller for easy deployment
...@@ -20,6 +21,18 @@ A cross-platform multimedia client application with video playback, web dashboar ...@@ -20,6 +21,18 @@ A cross-platform multimedia client application with video playback, web dashboar
## Recent Improvements ## Recent Improvements
### Version 1.2.4 (August 2025)
-**Screen Casting Integration**: Complete screen capture and Chromecast streaming functionality integrated into MBetterC application
-**ScreenCastComponent**: New threaded component with HTTP server, FFmpeg screen capture, and Chromecast device management
-**Web-Based Screen Cast Control**: Comprehensive web interface at `/screen_cast` with device discovery, streaming controls, and real-time status
-**Chromecast Device Discovery**: Automatic discovery and management of Chromecast devices on the local network
-**Cross-Platform Screen Capture**: FFmpeg-based screen recording with platform-specific audio/video input sources for Linux, Windows, macOS
-**Administrative Settings**: Screen cast configuration panel in web dashboard with quality, bitrate, and network settings
-**Network Stream URLs**: Automatically generated network-accessible streaming URLs for Chromecast compatibility
-**Real-Time Status Updates**: Live status monitoring with proper button state management and streaming feedback
-**Command-Line Control**: Screen casting enabled by default with `--no-screen-cast` flag for opt-out configuration
### Version 1.2.3 (August 2025) ### Version 1.2.3 (August 2025)
-**Boxing Match Database**: Added comprehensive `matches` and `match_outcomes` database tables adapted from mbetterd MySQL schema -**Boxing Match Database**: Added comprehensive `matches` and `match_outcomes` database tables adapted from mbetterd MySQL schema
...@@ -79,12 +92,13 @@ A cross-platform multimedia client application with video playback, web dashboar ...@@ -79,12 +92,13 @@ A cross-platform multimedia client application with video playback, web dashboar
## Architecture ## Architecture
The application consists of four main threads: The application consists of five main threads:
1. **PyQt Thread**: Video player with overlay rendering 1. **PyQt Thread**: Video player with overlay rendering
2. **Web Dashboard Thread**: Flask-based web interface 2. **Web Dashboard Thread**: Flask-based web interface
3. **REST API Client Thread**: External API communication 3. **REST API Client Thread**: External API communication
4. **Main Loop Thread**: Inter-thread message coordination 4. **Screen Cast Thread**: Screen capture, HTTP streaming, and Chromecast management
5. **Main Loop Thread**: Inter-thread message coordination
## Quick Start ## Quick Start
...@@ -106,12 +120,15 @@ pip install -r requirements.txt ...@@ -106,12 +120,15 @@ pip install -r requirements.txt
### Running the Application ### Running the Application
```bash ```bash
# Run in fullscreen mode (default) # Run in fullscreen mode with screen casting enabled (default)
python main.py python main.py
# Run in windowed mode # Run in windowed mode with screen casting
python main.py --no-fullscreen python main.py --no-fullscreen
# Disable screen casting functionality
python main.py --no-screen-cast
# Use native Qt overlays instead of WebEngine # Use native Qt overlays instead of WebEngine
python main.py --overlay-type native python main.py --overlay-type native
...@@ -136,6 +153,7 @@ python build.py ...@@ -136,6 +153,7 @@ python build.py
Configuration is stored in SQLite database with automatic versioning. Access the web dashboard at `http://localhost:5001` (default) to configure: Configuration is stored in SQLite database with automatic versioning. Access the web dashboard at `http://localhost:5001` (default) to configure:
- Video overlay templates - Video overlay templates
- Screen casting settings and Chromecast devices
- REST API endpoints and tokens - REST API endpoints and tokens
- User authentication - User authentication
- System settings - System settings
...@@ -166,7 +184,7 @@ mbetterc/ ...@@ -166,7 +184,7 @@ mbetterc/
│ ├── qt_player/ # PyQt video player │ ├── qt_player/ # PyQt video player
│ ├── web_dashboard/ # Flask web interface │ ├── web_dashboard/ # Flask web interface
│ ├── api_client/ # REST API client │ ├── api_client/ # REST API client
│ ├── core/ # Main loop and message handling │ ├── core/ # Main loop, message handling, and screen casting
│ └── utils/ # Utility functions │ └── utils/ # Utility functions
├── assets/ # Static assets (images, templates) ├── assets/ # Static assets (images, templates)
├── templates/ # Video overlay templates (built-in) ├── templates/ # Video overlay templates (built-in)
...@@ -255,6 +273,12 @@ Threads communicate via Python Queues with structured messages: ...@@ -255,6 +273,12 @@ Threads communicate via Python Queues with structured messages:
- `SYSTEM_STATUS` - Component status update - `SYSTEM_STATUS` - Component status update
- `LOG_ENTRY` - Log entry for database storage - `LOG_ENTRY` - Log entry for database storage
#### Screen Cast Messages
- `WEB_ACTION` - Web dashboard screen cast control actions
- `SCREEN_CAST_START` - Start screen capture and streaming
- `SCREEN_CAST_STOP` - Stop screen capture and streaming
- `SCREEN_CAST_STATUS` - Screen cast component status update
## Troubleshooting ## Troubleshooting
### Common Issues ### Common Issues
......
...@@ -145,6 +145,28 @@ Examples: ...@@ -145,6 +145,28 @@ Examples:
help='Use native Qt overlay instead of QWebEngineView (prevents freezing on some systems)' help='Use native Qt overlay instead of QWebEngineView (prevents freezing on some systems)'
) )
# Screen cast options
parser.add_argument(
'--no-screen-cast',
action='store_true',
default=False,
help='Disable screen capture and Chromecast streaming functionality'
)
parser.add_argument(
'--screen-cast-port',
type=int,
default=8000,
help='Port for screen cast HTTP server (default: 8000)'
)
parser.add_argument(
'--chromecast-name',
type=str,
default=None,
help='Name of Chromecast device to connect to (auto-discover if not specified)'
)
parser.add_argument( parser.add_argument(
'--version', '--version',
action='version', action='version',
...@@ -196,6 +218,17 @@ def main(): ...@@ -196,6 +218,17 @@ def main():
settings.enable_web = not args.no_web settings.enable_web = not args.no_web
settings.qt.use_native_overlay = args.native_overlay settings.qt.use_native_overlay = args.native_overlay
# Screen cast settings
# Screen cast is enabled by default, disable only if --no-screen-cast is specified
settings.enable_screen_cast = not args.no_screen_cast
if not args.no_screen_cast:
settings.screen_cast.enabled = True
settings.screen_cast.stream_port = args.screen_cast_port
if args.chromecast_name:
settings.screen_cast.chromecast_name = args.chromecast_name
else:
settings.screen_cast.enabled = False
if args.db_path: if args.db_path:
settings.database_path = args.db_path settings.database_path = args.db_path
......
...@@ -38,6 +38,7 @@ class MbetterClientApplication: ...@@ -38,6 +38,7 @@ class MbetterClientApplication:
self.web_dashboard = None self.web_dashboard = None
self.api_client = None self.api_client = None
self.template_watcher = None self.template_watcher = None
self.screen_cast = None
# Main loop thread # Main loop thread
self._main_loop_thread: Optional[threading.Thread] = None self._main_loop_thread: Optional[threading.Thread] = None
...@@ -116,6 +117,7 @@ class MbetterClientApplication: ...@@ -116,6 +117,7 @@ class MbetterClientApplication:
stored_settings.database_path = self.settings.database_path stored_settings.database_path = self.settings.database_path
stored_settings.enable_qt = self.settings.enable_qt stored_settings.enable_qt = self.settings.enable_qt
stored_settings.enable_web = self.settings.enable_web stored_settings.enable_web = self.settings.enable_web
stored_settings.enable_screen_cast = self.settings.enable_screen_cast # Preserve screen cast setting
# Preserve command line Qt overlay setting # Preserve command line Qt overlay setting
stored_settings.qt.use_native_overlay = self.settings.qt.use_native_overlay stored_settings.qt.use_native_overlay = self.settings.qt.use_native_overlay
...@@ -207,6 +209,17 @@ class MbetterClientApplication: ...@@ -207,6 +209,17 @@ class MbetterClientApplication:
logger.error("API client initialization failed") logger.error("API client initialization failed")
return False return False
# Initialize screen cast component
if self.settings.enable_screen_cast:
logger.info("Initializing screen cast component...")
if self._initialize_screen_cast():
components_initialized += 1
else:
logger.error("Screen cast initialization failed")
return False
else:
logger.info("Screen cast component disabled")
if components_initialized == 0: if components_initialized == 0:
logger.error("No components were initialized") logger.error("No components were initialized")
return False return False
...@@ -290,6 +303,9 @@ class MbetterClientApplication: ...@@ -290,6 +303,9 @@ class MbetterClientApplication:
settings=self.settings.web settings=self.settings.web
) )
# Set reference to main application for component access
self.web_dashboard.set_main_application(self)
# Register with thread manager # Register with thread manager
self.thread_manager.register_component("web_dashboard", self.web_dashboard) self.thread_manager.register_component("web_dashboard", self.web_dashboard)
...@@ -322,6 +338,36 @@ class MbetterClientApplication: ...@@ -322,6 +338,36 @@ class MbetterClientApplication:
logger.error(f"API client initialization failed: {e}") logger.error(f"API client initialization failed: {e}")
return False return False
def _initialize_screen_cast(self) -> bool:
"""Initialize screen cast component"""
try:
from .screen_cast import ScreenCastComponent
# Use configuration from settings
config = self.settings.screen_cast
# Set output directory to user data dir if not specified
output_dir = config.output_dir
if not output_dir:
output_dir = str(self.settings.get_user_data_dir() / "screen_cast")
self.screen_cast = ScreenCastComponent(
message_bus=self.message_bus,
stream_port=config.stream_port,
chromecast_name=config.chromecast_name,
output_dir=output_dir
)
# Register with thread manager
self.thread_manager.register_component("screen_cast", self.screen_cast)
logger.info(f"Screen cast component initialized on port {config.stream_port}")
return True
except Exception as e:
logger.error(f"Screen cast initialization failed: {e}")
return False
def run(self) -> int: def run(self) -> int:
"""Run the application""" """Run the application"""
try: try:
......
This diff is collapsed.
...@@ -20,6 +20,7 @@ from ..database.manager import DatabaseManager ...@@ -20,6 +20,7 @@ from ..database.manager import DatabaseManager
from .auth import AuthManager from .auth import AuthManager
from .api import DashboardAPI from .api import DashboardAPI
from .routes import main_bp, auth_bp, api_bp from .routes import main_bp, auth_bp, api_bp
from .screen_cast_routes import screen_cast_bp
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -39,12 +40,17 @@ class WebDashboard(ThreadedComponent): ...@@ -39,12 +40,17 @@ class WebDashboard(ThreadedComponent):
self.server: Optional = None self.server: Optional = None
self.auth_manager: Optional[AuthManager] = None self.auth_manager: Optional[AuthManager] = None
self.api: Optional[DashboardAPI] = None self.api: Optional[DashboardAPI] = None
self.main_application = None # Will be set by the main application
# Register message queue # Register message queue
self.message_queue = self.message_bus.register_component(self.name) self.message_queue = self.message_bus.register_component(self.name)
logger.info("WebDashboard initialized") logger.info("WebDashboard initialized")
def set_main_application(self, app):
"""Set reference to main application for component access"""
self.main_application = app
def initialize(self) -> bool: def initialize(self) -> bool:
"""Initialize Flask application""" """Initialize Flask application"""
try: try:
...@@ -141,6 +147,7 @@ class WebDashboard(ThreadedComponent): ...@@ -141,6 +147,7 @@ class WebDashboard(ThreadedComponent):
g.message_bus = self.message_bus g.message_bus = self.message_bus
g.auth_manager = self.auth_manager g.auth_manager = self.auth_manager
g.api = self.api g.api = self.api
g.main_app = self.main_application # Provide access to main app
# Template context processors # Template context processors
@app.context_processor @app.context_processor
...@@ -181,10 +188,15 @@ class WebDashboard(ThreadedComponent): ...@@ -181,10 +188,15 @@ class WebDashboard(ThreadedComponent):
api_bp.config_manager = self.config_manager api_bp.config_manager = self.config_manager
api_bp.message_bus = self.message_bus api_bp.message_bus = self.message_bus
# Pass dependencies to screen cast blueprint
screen_cast_bp.message_bus = self.message_bus
screen_cast_bp.db_manager = self.db_manager
# Register blueprints # Register blueprints
self.app.register_blueprint(main_bp) self.app.register_blueprint(main_bp)
self.app.register_blueprint(auth_bp, url_prefix='/auth') self.app.register_blueprint(auth_bp, url_prefix='/auth')
self.app.register_blueprint(api_bp, url_prefix='/api') self.app.register_blueprint(api_bp, url_prefix='/api')
self.app.register_blueprint(screen_cast_bp, url_prefix='/screen_cast')
def _create_server(self): def _create_server(self):
"""Create HTTP server""" """Create HTTP server"""
......
...@@ -133,6 +133,7 @@ def configuration(): ...@@ -133,6 +133,7 @@ def configuration():
video_config = main_bp.config_manager.get_section_config("video") or {} video_config = main_bp.config_manager.get_section_config("video") or {}
database_config = main_bp.config_manager.get_section_config("database") or {} database_config = main_bp.config_manager.get_section_config("database") or {}
api_config = main_bp.config_manager.get_section_config("api") or {} api_config = main_bp.config_manager.get_section_config("api") or {}
screen_cast_config = main_bp.config_manager.get_section_config("screen_cast") or {}
config_data.update({ config_data.update({
# General settings # General settings
...@@ -153,7 +154,16 @@ def configuration(): ...@@ -153,7 +154,16 @@ def configuration():
'api_token': api_config.get('api_token', ''), 'api_token': api_config.get('api_token', ''),
'api_interval': api_config.get('api_interval', 1800), 'api_interval': api_config.get('api_interval', 1800),
'api_timeout': api_config.get('api_timeout', 30), 'api_timeout': api_config.get('api_timeout', 30),
'api_enabled': api_config.get('api_enabled', True) 'api_enabled': api_config.get('api_enabled', True),
# Screen cast settings
'screen_cast_enabled': screen_cast_config.get('enabled', True),
'screen_cast_port': screen_cast_config.get('stream_port', 8000),
'chromecast_name': screen_cast_config.get('chromecast_name', ''),
'screen_cast_resolution': screen_cast_config.get('resolution', '1280x720'),
'screen_cast_framerate': screen_cast_config.get('framerate', 15),
'screen_cast_auto_start_capture': screen_cast_config.get('auto_start_capture', False),
'screen_cast_auto_start_streaming': screen_cast_config.get('auto_start_streaming', False)
}) })
except Exception as e: except Exception as e:
logger.warning(f"Error loading configuration values: {e}") logger.warning(f"Error loading configuration values: {e}")
...@@ -170,7 +180,14 @@ def configuration(): ...@@ -170,7 +180,14 @@ def configuration():
'api_token': '', 'api_token': '',
'api_interval': 1800, 'api_interval': 1800,
'api_timeout': 30, 'api_timeout': 30,
'api_enabled': True 'api_enabled': True,
'screen_cast_enabled': True,
'screen_cast_port': 8000,
'chromecast_name': '',
'screen_cast_resolution': '1280x720',
'screen_cast_framerate': 15,
'screen_cast_auto_start_capture': False,
'screen_cast_auto_start_streaming': False
} }
return render_template('dashboard/config.html', return render_template('dashboard/config.html',
......
This diff is collapsed.
...@@ -56,6 +56,12 @@ ...@@ -56,6 +56,12 @@
<i class="fas fa-upload me-1"></i>Video Test <i class="fas fa-upload me-1"></i>Video Test
</a> </a>
</li> </li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'screen_cast.screen_cast_dashboard' %}active{% endif %}"
href="{{ url_for('screen_cast.screen_cast_dashboard') }}">
<i class="fas fa-cast me-1"></i>Screen Cast
</a>
</li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link {% if request.endpoint == 'main.api_tokens' %}active{% endif %}" <a class="nav-link {% if request.endpoint == 'main.api_tokens' %}active{% endif %}"
href="{{ url_for('main.api_tokens') }}"> href="{{ url_for('main.api_tokens') }}">
......
...@@ -115,8 +115,92 @@ ...@@ -115,8 +115,92 @@
</div> </div>
</div> </div>
<!-- Screen Cast Settings -->
<div class="card mb-4">
<div class="card-header">
<h5>Screen Cast Settings</h5>
</div>
<div class="card-body">
<form id="screen-cast-config-form">
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="screen-cast-enabled"
{% if config.screen_cast_enabled != false %}checked{% endif %}>
<label class="form-check-label" for="screen-cast-enabled">
Enable Screen Cast & Chromecast Streaming
</label>
<div class="form-text">Enable screen capture and Chromecast streaming functionality</div>
</div>
<div class="mb-3">
<label for="screen-cast-port" class="form-label">Stream Port</label>
<input type="number" class="form-control" id="screen-cast-port"
value="{{ config.screen_cast_port or 8000 }}" min="1024" max="65535">
<div class="form-text">Port for the HTTP streaming server (1024-65535)</div>
</div>
<div class="mb-3">
<label for="chromecast-name" class="form-label">Preferred Chromecast Device</label>
<input type="text" class="form-control" id="chromecast-name"
value="{{ config.chromecast_name or '' }}"
placeholder="Leave empty for auto-discovery">
<div class="form-text">Name of preferred Chromecast device (optional)</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="screen-cast-resolution" class="form-label">Capture Resolution</label>
<select class="form-select" id="screen-cast-resolution">
<option value="1920x1080" {% if config.screen_cast_resolution == '1920x1080' %}selected{% endif %}>1920x1080 (Full HD)</option>
<option value="1280x720" {% if config.screen_cast_resolution == '1280x720' or not config.screen_cast_resolution %}selected{% endif %}>1280x720 (HD)</option>
<option value="854x480" {% if config.screen_cast_resolution == '854x480' %}selected{% endif %}>854x480 (SD)</option>
<option value="640x360" {% if config.screen_cast_resolution == '640x360' %}selected{% endif %}>640x360 (Low)</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="screen-cast-framerate" class="form-label">Frame Rate (FPS)</label>
<select class="form-select" id="screen-cast-framerate">
<option value="30" {% if config.screen_cast_framerate == 30 %}selected{% endif %}>30 FPS</option>
<option value="24" {% if config.screen_cast_framerate == 24 %}selected{% endif %}>24 FPS</option>
<option value="15" {% if config.screen_cast_framerate == 15 or not config.screen_cast_framerate %}selected{% endif %}>15 FPS</option>
<option value="10" {% if config.screen_cast_framerate == 10 %}selected{% endif %}>10 FPS</option>
</select>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="screen-cast-auto-start-capture"
{% if config.screen_cast_auto_start_capture %}checked{% endif %}>
<label class="form-check-label" for="screen-cast-auto-start-capture">
Auto-start Screen Capture
</label>
<div class="form-text">Automatically start capturing when application starts</div>
</div>
</div>
<div class="col-md-6">
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="screen-cast-auto-start-streaming"
{% if config.screen_cast_auto_start_streaming %}checked{% endif %}>
<label class="form-check-label" for="screen-cast-auto-start-streaming">
Auto-start Chromecast Streaming
</label>
<div class="form-text">Automatically start streaming when capture begins</div>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">Save Screen Cast Settings</button>
</form>
</div>
</div>
<!-- Database Settings --> <!-- Database Settings -->
<div class="card"> <div class="card mb-4">
<div class="card-header"> <div class="card-header">
<h5>Database Settings</h5> <h5>Database Settings</h5>
</div> </div>
...@@ -205,6 +289,23 @@ ...@@ -205,6 +289,23 @@
saveConfig('api', config); saveConfig('api', config);
}); });
// Save screen cast configuration
document.getElementById('screen-cast-config-form').addEventListener('submit', function(e) {
e.preventDefault();
const config = {
enabled: document.getElementById('screen-cast-enabled').checked,
stream_port: parseInt(document.getElementById('screen-cast-port').value),
chromecast_name: document.getElementById('chromecast-name').value,
resolution: document.getElementById('screen-cast-resolution').value,
framerate: parseInt(document.getElementById('screen-cast-framerate').value),
auto_start_capture: document.getElementById('screen-cast-auto-start-capture').checked,
auto_start_streaming: document.getElementById('screen-cast-auto-start-streaming').checked
};
saveConfig('screen_cast', config);
});
// Test API connection // Test API connection
document.getElementById('test-api-connection').addEventListener('click', function() { document.getElementById('test-api-connection').addEventListener('click', function() {
const url = document.getElementById('fastapi-url').value; const url = document.getElementById('fastapi-url').value;
......
...@@ -33,6 +33,10 @@ watchdog>=3.0.0 ...@@ -33,6 +33,10 @@ watchdog>=3.0.0
opencv-python>=4.5.0 opencv-python>=4.5.0
Pillow>=9.0.0 Pillow>=9.0.0
# Screen capture and streaming (optional dependencies)
ffmpeg-python>=0.2.0
pychromecast>=13.0.0
# Logging # Logging
loguru>=0.7.0 loguru>=0.7.0
......
import ffmpeg
import threading
import http.server
import socketserver
import pychromecast
import time
import platform
import os
import sys
# HTTP server to serve the stream
class StreamHandler(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header("Content-type", "video/mp4")
self.end_headers()
try:
with open("stream.mp4", "rb") as f:
self.wfile.write(f.read())
except FileNotFoundError:
self.send_error(404, "Stream not ready")
def http_and_chromecast_thread():
# Start HTTP server
port = 8000
server = socketserver.TCPServer(("", port), StreamHandler)
server_thread = threading.Thread(target=server.serve_forever)
server_thread.daemon = True
server_thread.start()
print(f"HTTP server running on http://localhost:{port}")
# Initialize Chromecast
chromecasts, browser = pychromecast.get_listed_chromecasts(friendly_names=["Living Room"]) # Replace with your Chromecast name
if not chromecasts:
print("No Chromecast found")
return
cast = chromecasts[0]
cast.wait()
print(f"Connected to Chromecast: {cast.device.friendly_name}")
# Play stream
mc = cast.media_controller
stream_url = f"http://localhost:{port}/stream.mp4"
mc.play_media(stream_url, "video/mp4")
mc.block_until_active()
# Keep thread alive
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
mc.stop()
cast.quit_app()
pychromecast.discovery.stop_discovery(browser)
server.shutdown()
server.server_close()
def ffmpeg_capture_thread():
system = platform.system()
output_file = "stream.mp4"
while True:
try:
# Video input (platform-specific)
if system == "Linux":
video = ffmpeg.input(":0.0+0,0", format="x11grab", s="1280x720")
elif system == "Windows":
video = ffmpeg.input("desktop", format="gdigrab", s="1280x720")
elif system == "Darwin": # macOS
video = ffmpeg.input("1:none", format="avfoundation", capture_cursor=1, s="1280x720")
else:
print(f"Unsupported platform: {system}")
return
# Audio input (platform-specific)
audio = None
if system == "Linux":
try:
# Try PulseAudio first
audio = ffmpeg.input("default", format="pulse")
except Exception:
print("PulseAudio failed, falling back to ALSA")
audio = ffmpeg.input("hw:0", format="alsa") # Adjust device if needed
elif system == "Windows":
audio = ffmpeg.input("audio=Stereo Mix", format="dshow") # Adjust to your audio device
elif system == "Darwin":
audio = ffmpeg.input("0", format="avfoundation") # Adjust to BlackHole or microphone
# Combine video and audio streams
stream = ffmpeg.output(
video, audio, output_file, format="mp4", vcodec="libx264", acodec="aac",
pix_fmt="yuv420p", r=15, preset="ultrafast", tune="zerolatency",
movflags="frag_keyframe+empty_moov", ac=2, ar=44100
)
stream = ffmpeg.overwrite_output(stream)
# Run FFmpeg
process = stream.run_async()
process.wait()
print("FFmpeg stopped unexpectedly")
except Exception as e:
print(f"FFmpeg error: {e}")
finally:
if os.path.exists(output_file):
os.remove(output_file) # Clean up before restart
print("Restarting FFmpeg in 5 seconds...")
time.sleep(5)
def main():
# Start HTTP server and Chromecast thread
threading.Thread(target=http_and_chromecast_thread, daemon=True).start()
# Start FFmpeg capture thread
threading.Thread(target=ffmpeg_capture_thread, daemon=True).start()
# Main thread: empty loop
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print("Exiting...")
sys.exit(0)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Test script for Screen Cast integration in MbetterClient
Tests the integration without actually starting FFmpeg or Chromecast
"""
import sys
import logging
from pathlib import Path
# Add the project root to Python path
project_root = Path(__file__).parent
sys.path.insert(0, str(project_root))
from mbetterclient.config.settings import AppSettings
from mbetterclient.core.application import MbetterClientApplication
from mbetterclient.core.message_bus import MessageBus
from mbetterclient.core.screen_cast import ScreenCastComponent
def setup_logging():
"""Setup basic logging for testing"""
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
return logging.getLogger(__name__)
def test_screen_cast_config():
"""Test screen cast configuration"""
logger = logging.getLogger("test_config")
logger.info("Testing ScreenCast configuration...")
# Test default settings
settings = AppSettings()
assert hasattr(settings, 'screen_cast'), "ScreenCast config not found in AppSettings"
assert hasattr(settings, 'enable_screen_cast'), "enable_screen_cast not found in AppSettings"
# Test configuration values
sc_config = settings.screen_cast
assert sc_config.stream_port == 8000, f"Expected port 8000, got {sc_config.stream_port}"
assert sc_config.resolution == "1280x720", f"Expected 1280x720, got {sc_config.resolution}"
assert sc_config.framerate == 15, f"Expected 15fps, got {sc_config.framerate}"
logger.info("✓ ScreenCast configuration test passed")
return True
def test_screen_cast_component():
"""Test screen cast component initialization"""
logger = logging.getLogger("test_component")
logger.info("Testing ScreenCast component initialization...")
# Create message bus
message_bus = MessageBus()
message_bus.register_component("test")
# Create screen cast component
try:
screen_cast = ScreenCastComponent(
message_bus=message_bus,
stream_port=8001, # Use different port for testing
chromecast_name="Test Device",
output_dir="/tmp/test_screen_cast"
)
# Test component attributes
assert screen_cast.stream_port == 8001
assert screen_cast.chromecast_name == "Test Device"
assert screen_cast.name == "screen_cast"
assert not screen_cast.capture_active
assert not screen_cast.streaming_active
logger.info("✓ ScreenCast component initialization test passed")
return True
except Exception as e:
logger.error(f"✗ ScreenCast component test failed: {e}")
return False
def test_application_integration():
"""Test application integration"""
logger = logging.getLogger("test_integration")
logger.info("Testing MbetterClient application integration...")
try:
# Test direct component integration with thread manager
from mbetterclient.core.thread_manager import ThreadManager
from mbetterclient.core.message_bus import MessageBus
from mbetterclient.config.settings import AppSettings
from mbetterclient.core.screen_cast import ScreenCastComponent
# Create components
settings = AppSettings()
message_bus = MessageBus()
message_bus.register_component("core")
thread_manager = ThreadManager(message_bus, settings)
# Create and register screen cast component
screen_cast = ScreenCastComponent(
message_bus=message_bus,
stream_port=8001,
chromecast_name="Test Device",
output_dir="/tmp/test_screen_cast"
)
thread_manager.register_component("screen_cast", screen_cast)
# Test that component is registered
if "screen_cast" in thread_manager.get_component_names():
logger.info("✓ ScreenCast component registered with thread manager")
# Test component status
status = thread_manager.get_component_status("screen_cast")
if status and status["name"] == "screen_cast":
logger.info("✓ ScreenCast component status available")
return True
else:
logger.error("✗ ScreenCast component status not available")
return False
else:
logger.error("✗ ScreenCast component not registered")
return False
except Exception as e:
logger.error(f"✗ Application integration test failed: {e}")
return False
def test_web_routes():
"""Test web dashboard routes"""
logger = logging.getLogger("test_web")
logger.info("Testing web dashboard routes...")
try:
from mbetterclient.web_dashboard.screen_cast_routes import screen_cast_bp
# Check that blueprint was imported successfully
assert screen_cast_bp is not None, "screen_cast_bp is None"
assert screen_cast_bp.name == "screen_cast", f"Expected 'screen_cast', got {screen_cast_bp.name}"
# Test that the blueprint can be registered (basic functionality test)
from flask import Flask
test_app = Flask(__name__)
# This will raise an exception if the blueprint is malformed
test_app.register_blueprint(screen_cast_bp, url_prefix='/screen_cast')
# Check that routes were registered
routes = [rule.rule for rule in test_app.url_map.iter_rules()]
# Check for at least one of our routes
screen_cast_routes = [r for r in routes if '/screen_cast' in r]
if screen_cast_routes:
logger.info(f"✓ Found screen cast routes: {len(screen_cast_routes)} routes")
else:
logger.warning("No screen cast routes found, but blueprint registered successfully")
logger.info("✓ Web dashboard routes test passed")
return True
except Exception as e:
logger.error(f"✗ Web routes test failed: {e}")
return False
def test_dependencies():
"""Test that optional dependencies can be imported or fail gracefully"""
logger = logging.getLogger("test_deps")
logger.info("Testing optional dependencies...")
# Test ffmpeg-python import
try:
import ffmpeg
logger.info("✓ ffmpeg-python is available")
ffmpeg_available = True
except ImportError:
logger.warning("⚠ ffmpeg-python not available (this is expected if not installed)")
ffmpeg_available = False
# Test pychromecast import
try:
import pychromecast
logger.info("✓ pychromecast is available")
chromecast_available = True
except ImportError:
logger.warning("⚠ pychromecast not available (this is expected if not installed)")
chromecast_available = False
# The component should handle missing dependencies gracefully
logger.info("✓ Dependencies test passed (graceful handling)")
return True
def main():
"""Main test function"""
logger = setup_logging()
logger.info("=" * 60)
logger.info("Starting MbetterClient ScreenCast Integration Tests")
logger.info("=" * 60)
tests = [
("Configuration", test_screen_cast_config),
("Component", test_screen_cast_component),
("Application Integration", test_application_integration),
("Web Routes", test_web_routes),
("Dependencies", test_dependencies),
]
passed = 0
failed = 0
for test_name, test_func in tests:
logger.info(f"\nRunning {test_name} test...")
try:
if test_func():
passed += 1
logger.info(f"✓ {test_name} test PASSED")
else:
failed += 1
logger.error(f"✗ {test_name} test FAILED")
except Exception as e:
failed += 1
logger.error(f"✗ {test_name} test FAILED with exception: {e}")
logger.info("\n" + "=" * 60)
logger.info(f"Test Results: {passed} passed, {failed} failed")
logger.info("=" * 60)
if failed == 0:
logger.info("🎉 All tests passed! ScreenCast integration is working correctly.")
return 0
else:
logger.error(f"❌ {failed} test(s) failed. Please check the integration.")
return 1
if __name__ == "__main__":
sys.exit(main())
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment