Files
Artemis-Eapil/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.ts
T
somew1nd 9a5aaf99b7 feat: introduce MCP server, cross-platform runtime, and Showcase UI reorganization
- Showcase UI Reorganization:
  * Move web UI dashboard from root directory into apps/showcase_ui/ for cleaner project structure
  * Add 'artemis ui' CLI command (artemis/interfaces/cli/commands/ui.py) and launcher scripts for the interactive showcase console
- Standalone MCP Server & Notifiers (mcp_server/):
  * Implement dedicated MCP server package with mobile automation tools (device_state, inspect_trace, task_manager, task_runner)
  * Add multi-channel notifiers (AgentAPI, Desktop notifications, Webhooks, File output, and Composite routing)
  * Introduce background task runner and trace store for autonomous mobile execution monitoring
- Cross-Platform Runtime & Toolchain Support:
  * Add artemis/platform/ with OS-specific implementations for Linux, macOS (Darwin), and Windows
  * Add artemis/runtime/supervisor.py for resilient process and agent runtime supervision
  * Add artemis/toolchain/ with dependency resolution and SDK descriptors
  * Add cross-platform launcher scripts (start.sh, start.bat) and installer scripts (scripts/install_deps.*)
- Core Enhancements & Diagnostics:
  * Enhance coordinate normalization, OCR API fallback, and perception pipeline
  * Add credential validation (credentials_validator.py) and system diagnostics
  * Update bilingual documentation (README.md, README_CN.md) and architecture diagrams
- Test Suite Expansion:
  * Add integration tests for cloud gateway, remote ADB, and Artemis login
  * Add unit tests covering core diagnostics, platform detection, toolchain, credentials validator, optional OCR, and MCP tools
2026-08-18 13:59:27 -07:00

287 lines
7.5 KiB
TypeScript

/**
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
Component,
inject,
signal,
computed,
ElementRef,
ViewChild,
HostListener,
effect
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { AgentService } from '../../services/agent.service';
@Component({
selector: 'app-floating-video-player',
standalone: true,
imports: [CommonModule, FormsModule],
templateUrl: './floating-video-player.component.html',
styleUrl: './floating-video-player.component.scss'
})
export class FloatingVideoPlayerComponent {
public agentService = inject(AgentService);
@ViewChild('videoRef') videoRef?: ElementRef<HTMLVideoElement>;
// Playback state signals
public isPlaying = signal<boolean>(false);
public playbackRate = signal<number>(1.0);
public isLooping = signal<boolean>(false);
public currentTime = signal<number>(0);
public duration = signal<number>(0);
public isMuted = signal<boolean>(false);
public isTheaterMode = signal<boolean>(false);
public videoLoadError = signal<boolean>(false);
public liveStreamUrl = signal<string>('/api/stream/device-live');
public liveStreamError = signal<boolean>(false);
// Available speed options
public speedOptions = [0.5, 1.0, 1.5, 2.0, 4.0];
// Dragging and window positioning state
public posX = signal<number>(
typeof window !== 'undefined' ? Math.max(20, window.innerWidth - 380) : 100
);
public posY = signal<number>(
typeof window !== 'undefined' ? Math.max(60, window.innerHeight - 660) : 100
);
private isDragging = false;
private dragStartX = 0;
private dragStartY = 0;
private initialPosX = 0;
private initialPosY = 0;
constructor() {
// Reset video load error whenever active video URL changes
effect(
() => {
const url = this.agentService.activeVideoUrl();
this.videoLoadError.set(false);
this.currentTime.set(0);
this.duration.set(0);
this.isPlaying.set(false);
},
{ allowSignalWrites: true }
);
}
public onLiveStreamError(): void {
this.liveStreamError.set(true);
}
public retryLiveStream(): void {
this.liveStreamError.set(false);
this.liveStreamUrl.set(`/api/stream/device-live?t=${Date.now()}`);
}
/**
* Format seconds into mm:ss format
*/
public formatTime(seconds: number): string {
if (isNaN(seconds) || seconds < 0) return '00:00';
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
const mStr = mins < 10 ? '0' + mins : String(mins);
const sStr = secs < 10 ? '0' + secs : String(secs);
return `${mStr}:${sStr}`;
}
/**
* Progress percentage for timeline scrubber (0 - 100)
*/
public progressPercent = computed(() => {
const dur = this.duration();
if (dur <= 0) return 0;
return Math.min(100, Math.max(0, (this.currentTime() / dur) * 100));
});
/**
* Start dragging the window from header
*/
public startDrag(event: MouseEvent): void {
if (this.isTheaterMode()) return;
this.isDragging = true;
this.dragStartX = event.clientX;
this.dragStartY = event.clientY;
this.initialPosX = this.posX();
this.initialPosY = this.posY();
event.preventDefault();
}
@HostListener('window:mousemove', ['$event'])
public onDrag(event: MouseEvent): void {
if (!this.isDragging) return;
const deltaX = event.clientX - this.dragStartX;
const deltaY = event.clientY - this.dragStartY;
const minX = 10;
const minY = 10;
const maxX = Math.max(10, window.innerWidth - 240);
const maxY = Math.max(10, window.innerHeight - 80);
const targetX = Math.max(minX, Math.min(maxX, this.initialPosX + deltaX));
const targetY = Math.max(minY, Math.min(maxY, this.initialPosY + deltaY));
this.posX.set(targetX);
this.posY.set(targetY);
}
@HostListener('window:mouseup')
public stopDrag(): void {
this.isDragging = false;
}
@HostListener('window:resize')
public onResize(): void {
// Keep window within viewport bounds if window resized
const maxX = Math.max(10, window.innerWidth - 240);
const maxY = Math.max(10, window.innerHeight - 80);
if (this.posX() > maxX) this.posX.set(maxX);
if (this.posY() > maxY) this.posY.set(maxY);
}
@HostListener('window:keydown', ['$event'])
public onKeyDown(event: KeyboardEvent): void {
if (!this.agentService.isVideoWindowOpen()) return;
if (event.key === 'Escape') {
if (this.isTheaterMode()) {
this.isTheaterMode.set(false);
} else {
this.agentService.closeVideoPlayer();
}
}
}
/**
* Video element event handlers
*/
public onLoadedMetadata(): void {
const v = this.videoRef?.nativeElement;
if (v) {
this.duration.set(v.duration || 0);
this.videoLoadError.set(false);
v.playbackRate = this.playbackRate();
v.loop = this.isLooping();
}
}
public onTimeUpdate(): void {
const v = this.videoRef?.nativeElement;
if (v) {
this.currentTime.set(v.currentTime);
if (v.duration && v.duration !== this.duration()) {
this.duration.set(v.duration);
}
}
}
public onPlay(): void {
this.isPlaying.set(true);
}
public onPause(): void {
this.isPlaying.set(false);
}
public onEnded(): void {
this.isPlaying.set(false);
}
public onError(): void {
this.videoLoadError.set(true);
this.isPlaying.set(false);
}
/**
* Playback actions
*/
public togglePlay(): void {
const v = this.videoRef?.nativeElement;
if (!v) return;
if (v.paused) {
v.play().catch(() => {});
} else {
v.pause();
}
}
public restart(): void {
const v = this.videoRef?.nativeElement;
if (!v) return;
v.currentTime = 0;
v.play().catch(() => {});
}
public seek(seconds: number): void {
const v = this.videoRef?.nativeElement;
if (!v) return;
v.currentTime = Math.max(0, Math.min(this.duration(), seconds));
}
public onScrub(event: Event): void {
const input = event.target as HTMLInputElement;
const targetTime = parseFloat(input.value);
this.seek(targetTime);
}
public setSpeed(rate: number): void {
this.playbackRate.set(rate);
const v = this.videoRef?.nativeElement;
if (v) {
v.playbackRate = rate;
}
}
public toggleLoop(): void {
const next = !this.isLooping();
this.isLooping.set(next);
const v = this.videoRef?.nativeElement;
if (v) {
v.loop = next;
}
}
public toggleMute(): void {
const next = !this.isMuted();
this.isMuted.set(next);
const v = this.videoRef?.nativeElement;
if (v) {
v.muted = next;
}
}
public toggleTheater(): void {
this.isTheaterMode.set(!this.isTheaterMode());
}
public toggleMinimize(): void {
this.agentService.isVideoMinimized.set(!this.agentService.isVideoMinimized());
}
public openInNewTab(): void {
const url = this.agentService.activeVideoUrl();
if (url) {
window.open(url, '_blank');
}
}
}