diff --git a/src/app/googDevice/toolbox/GoogToolBox.ts b/src/app/googDevice/toolbox/GoogToolBox.ts index 83aeb8dd..459e770a 100644 --- a/src/app/googDevice/toolbox/GoogToolBox.ts +++ b/src/app/googDevice/toolbox/GoogToolBox.ts @@ -82,6 +82,12 @@ export class GoogToolBox extends ToolBox { elements.push(screenshot); } + const fullscreen = new ToolBoxButton('Fullscreen Mode', SvgImage.Icon.FULL_SCREEN); + fullscreen.addEventListener('click', () => { + player.openFullscreen(client); + }); + elements.push(fullscreen); + const keyboard = new ToolBoxCheckbox( 'Capture keyboard', SvgImage.Icon.KEYBOARD, diff --git a/src/app/player/BaseCanvasBasedPlayer.ts b/src/app/player/BaseCanvasBasedPlayer.ts index 613ada36..b7b4fc3e 100644 --- a/src/app/player/BaseCanvasBasedPlayer.ts +++ b/src/app/player/BaseCanvasBasedPlayer.ts @@ -163,6 +163,10 @@ export abstract class BaseCanvasBasedPlayer extends BasePlayer { if (parent) { const tag = BaseCanvasBasedPlayer.createElement(this.tag.id); tag.className = this.tag.className; + if (this.isFullScreen) { + tag.style.maxWidth = '100vw'; + tag.style.maxHeight = '100vh'; + } parent.replaceChild(tag, this.tag); parent.appendChild(this.touchableCanvas); this.tag = tag; diff --git a/src/app/player/BasePlayer.ts b/src/app/player/BasePlayer.ts index ab881ce5..de726e2e 100644 --- a/src/app/player/BasePlayer.ts +++ b/src/app/player/BasePlayer.ts @@ -5,6 +5,7 @@ import Size from '../Size'; import Util from '../Util'; import { TypedEmitter } from '../../common/TypedEmitter'; import { DisplayInfo } from '../DisplayInfo'; +import { StreamClientScrcpy } from '../googDevice/client/StreamClientScrcpy'; interface BitrateStat { timestamp: number; @@ -83,6 +84,7 @@ export abstract class BasePlayer extends TypedEmitter { private statLines: string[] = []; public readonly supportsScreenshot: boolean = false; public readonly resizeVideoToBounds: boolean = false; + protected isFullScreen: boolean = false; protected videoHeight = -1; protected videoWidth = -1; @@ -343,6 +345,123 @@ export abstract class BasePlayer extends TypedEmitter { return this.touchableCanvas; } + private static createVideoSettingsWithBounds(old: VideoSettings, newBounds: Size): VideoSettings { + return new VideoSettings({ + crop: old.crop, + bitrate: old.bitrate, + bounds: newBounds, + maxFps: old.maxFps, + iFrameInterval: old.iFrameInterval, + sendFrameMeta: old.sendFrameMeta, + lockedVideoOrientation: old.lockedVideoOrientation, + displayId: old.displayId, + codecOptions: old.codecOptions, + encoderName: old.encoderName, + }); + } + + public openFullscreen(client: StreamClientScrcpy) { + if (!this.parentElement) { + console.warn('Cannot enter fullscreen: no parent element'); + return; + } + + const element = this.parentElement as any; + + // Set up fullscreen styles that maintain an aspect ratio + const scaleScreen = () => { + if (!this.parentElement) return; + + // Scale the canvas to fit screen while maintaining aspect ratio + if (this.screenInfo) { + const { width: deviceWidth, height: deviceHeight } = this.screenInfo.videoSize; + console.log(`Device video size: ${deviceWidth}x${deviceHeight}`); + const ratio = window.devicePixelRatio || 1; + const screenWidth = window.screen.width; + const screenHeight = window.screen.height; + console.log(`Screen size: ${screenWidth}x${screenHeight}`); + let newBounds; + if (this.isFullScreen) { + newBounds = new Size(screenWidth * ratio, screenHeight * ratio); + this.parentElement.style.display = 'flex'; + this.parentElement.style.justifyContent = 'center'; + this.parentElement.style.alignItems = 'center'; + this.tag.style.maxWidth = '100vw'; + this.tag.style.maxHeight = '100vh'; + this.touchableCanvas.style.maxWidth = '100vw'; + this.touchableCanvas.style.maxHeight = '100vh'; + } else { + newBounds = client.getMaxSize(); + this.parentElement.style.display = ''; + this.parentElement.style.justifyContent = ''; + this.parentElement.style.alignItems = ''; + this.tag.style.maxWidth = ''; + this.tag.style.maxHeight = ''; + this.touchableCanvas.style.maxWidth = ''; + this.touchableCanvas.style.maxHeight = ''; + } + console.log('New bounds:', newBounds); + if (newBounds) { + client.sendNewVideoSetting(BasePlayer.createVideoSettingsWithBounds(this.videoSettings, newBounds)); + } + } + }; + + // Listen for fullscreen change events + const handleFullscreenChange = () => { + const isFullscreen = !!( + document.fullscreenElement || + (document as any).webkitFullscreenElement || + (document as any).mozFullScreenElement || + (document as any).msFullscreenElement + ); + + if (isFullscreen) { + this.isFullScreen = true; + scaleScreen(); + } else { + this.isFullScreen = false; + scaleScreen(); + // Remove event listeners + document.removeEventListener('fullscreenchange', handleFullscreenChange); + document.removeEventListener('webkitfullscreenchange', handleFullscreenChange); + document.removeEventListener('mozfullscreenchange', handleFullscreenChange); + document.removeEventListener('MSFullscreenChange', handleFullscreenChange); + } + }; + + // Add event listeners for fullscreen changes + document.addEventListener('fullscreenchange', handleFullscreenChange); + document.addEventListener('webkitfullscreenchange', handleFullscreenChange); + document.addEventListener('mozfullscreenchange', handleFullscreenChange); + document.addEventListener('MSFullscreenChange', handleFullscreenChange); + + try { + if (element.requestFullscreen) { + element.requestFullscreen(); + } else if (element.webkitRequestFullscreen) { + // Chrome, Safari, Opera (older) + element.webkitRequestFullscreen(); + } else if (element.webkitRequestFullScreen) { + // Safari (very old) + element.webkitRequestFullScreen(); + } else if (element.mozRequestFullScreen) { + // Firefox older + element.mozRequestFullScreen(); + } else if (element.msRequestFullscreen) { + // IE/Edge + element.msRequestFullscreen(); + } else if (element.oRequestFullscreen) { + // Opera (very old) + element.oRequestFullscreen(); + } else { + console.warn('Fullscreen API is not supported by this browser'); + } + } catch (error) { + console.error('Failed to enter fullscreen:', error); + } + } + public setParent(parent: HTMLElement): void { this.parentElement = parent; parent.appendChild(this.tag); diff --git a/src/app/ui/SvgImage.ts b/src/app/ui/SvgImage.ts index 392470ed..55bf44b9 100644 --- a/src/app/ui/SvgImage.ts +++ b/src/app/ui/SvgImage.ts @@ -15,6 +15,8 @@ import MenuSVG from '../../public/images/buttons/menu.svg'; import ArrowBackSVG from '../../public/images/buttons/arrow_back.svg'; import ToggleOnSVG from '../../public/images/buttons/toggle_on.svg'; import ToggleOffSVG from '../../public/images/buttons/toggle_off.svg'; +import FullScreenSVG from '../../public/images/buttons/full_screen.svg'; + export enum Icon { BACK, @@ -25,6 +27,7 @@ export enum Icon { VOLUME_DOWN, MORE, CAMERA, + FULL_SCREEN, KEYBOARD, CANCEL, OFFLINE, @@ -40,6 +43,8 @@ export default class SvgImage { static Icon = Icon; private static getSvgString(type: Icon): string { switch (type) { + case Icon.FULL_SCREEN: + return FullScreenSVG case Icon.KEYBOARD: return KeyboardSVG; case Icon.MORE: diff --git a/src/public/images/buttons/full_screen.svg b/src/public/images/buttons/full_screen.svg new file mode 100644 index 00000000..30247b96 --- /dev/null +++ b/src/public/images/buttons/full_screen.svg @@ -0,0 +1,9 @@ + + + \ No newline at end of file