From 1cef3c17a4be6683c69d9c1532dbb7eac361a0a5 Mon Sep 17 00:00:00 2001 From: Carlos Santos <4a.santos@gmail.com> Date: Mon, 22 Sep 2025 20:06:25 +0200 Subject: [PATCH] ov-components: enhance layout service with responsive viewport handling and layout options adjustment --- .../src/lib/services/layout/layout.service.ts | 182 ++++++++++++++++-- 1 file changed, 163 insertions(+), 19 deletions(-) diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/layout/layout.service.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/layout/layout.service.ts index 5a68e14e..862dbad0 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/layout/layout.service.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/layout/layout.service.ts @@ -1,8 +1,9 @@ -import { Injectable } from '@angular/core'; +import { Injectable, effect } from '@angular/core'; import { BehaviorSubject, Observable } from 'rxjs'; import { LayoutAlignment, LayoutClass, OpenViduLayout, OpenViduLayoutOptions } from '../../models/layout.model'; import { ILogger } from '../../models/logger.model'; import { LoggerService } from '../logger/logger.service'; +import { ViewportService } from '../viewport/viewport.service'; /** * @internal @@ -16,14 +17,19 @@ export class LayoutService { captionsTogglingObs: Observable; protected layoutWidth: BehaviorSubject = new BehaviorSubject(0); protected openviduLayout: OpenViduLayout | undefined; - protected openviduLayoutOptions: OpenViduLayoutOptions; + protected openviduLayoutOptions!: OpenViduLayoutOptions; protected captionsToggling: BehaviorSubject = new BehaviorSubject(false); protected log: ILogger; - constructor(protected loggerSrv: LoggerService) { + constructor( + protected loggerSrv: LoggerService, + protected viewportSrv: ViewportService + ) { this.layoutWidthObs = this.layoutWidth.asObservable(); this.captionsTogglingObs = this.captionsToggling.asObservable(); this.log = this.loggerSrv.get('LayoutService'); + this.openviduLayoutOptions = this.getOptions(); + this.setupViewportListener(); } initialize(container: HTMLElement) { @@ -43,6 +49,7 @@ export class LayoutService { update(timeout: number | undefined = undefined) { const updateAux = () => { if (this.openviduLayout && this.layoutContainer) { + this.openviduLayoutOptions = this.getOptions(); this.openviduLayout.updateLayout(this.layoutContainer, this.openviduLayoutOptions); this.sendLayoutWidthEvent(); } @@ -54,6 +61,10 @@ export class LayoutService { } } + updateResponsive() { + this.updateLayoutOptions(); + } + getLayout() { return this.openviduLayout; } @@ -62,27 +73,33 @@ export class LayoutService { this.openviduLayout = undefined; } + /** + * Get layout options adjusted to the current viewport + * @returns Layout options adjusted to the current viewport + */ protected getOptions(): OpenViduLayoutOptions { - const options = { - maxRatio: 3 / 2, // The narrowest ratio that will be used (default 2x3) - minRatio: 9 / 16, // The widest ratio that will be used (default 16x9) - fixedRatio: false /* If this is true then the aspect ratio of the video is maintained - and minRatio and maxRatio are ignored (default false) */, - bigClass: LayoutClass.BIG_ELEMENT, // The class to add to elements that should be sized bigger + const ratios = this.getResponsiveRatios(); + const percentages = this.getResponsivePercentages(); + + return { + maxRatio: ratios.maxRatio, + minRatio: ratios.minRatio, + fixedRatio: false, + bigClass: LayoutClass.BIG_ELEMENT, smallClass: LayoutClass.SMALL_ELEMENT, ignoredClass: LayoutClass.IGNORED_ELEMENT, - bigPercentage: 0.8, // The maximum percentage of space the big ones should take up - minBigPercentage: 0, // If this is set then it will scale down the big space if there is left over whitespace down to this minimum size - bigFixedRatio: false, // fixedRatio for the big ones - bigMaxRatio: 9 / 16, // The narrowest ratio to use for the big elements (default 2x3) - bigMinRatio: 9 / 16, // The widest ratio to use for the big elements (default 16x9) - bigFirst: true, // Whether to place the big one in the top left (true) or bottom right - animate: true, // Whether you want to animate the transitions. Deprecated property, to disable it remove the transaction property on OV_publisher css class + bigPercentage: percentages.bigPercentage, + minBigPercentage: percentages.minBigPercentage, + bigFixedRatio: false, + bigMaxRatio: ratios.bigMaxRatio, + bigMinRatio: ratios.bigMinRatio, + bigFirst: true, + animate: true, alignItems: LayoutAlignment.CENTER, bigAlignItems: LayoutAlignment.CENTER, smallAlignItems: LayoutAlignment.CENTER, - maxWidth: Infinity, // The maximum width of the elements - maxHeight: Infinity, // The maximum height of the elements + maxWidth: Infinity, + maxHeight: Infinity, smallMaxWidth: Infinity, smallMaxHeight: Infinity, bigMaxWidth: Infinity, @@ -90,7 +107,134 @@ export class LayoutService { scaleLastRow: true, bigScaleLastRow: true }; - return options; + } + + protected getResponsiveRatios() { + const isMobile = this.viewportSrv.isMobile(); + const isTablet = this.viewportSrv.isTablet(); + const isPortrait = this.viewportSrv.isPortrait(); + + if (isMobile && isPortrait) { + return { + maxRatio: 5 / 4, + minRatio: 4 / 5, + bigMaxRatio: 5 / 4, + bigMinRatio: 3 / 4 + }; + } + + if (isMobile) { + return { + maxRatio: 16 / 9, + minRatio: 3 / 4, + bigMaxRatio: 16 / 9, + bigMinRatio: 4 / 3 + }; + } + + if (isTablet && isPortrait) { + return { + maxRatio: 4 / 3, + minRatio: 3 / 5, + bigMaxRatio: 4 / 3, + bigMinRatio: 9 / 16 + }; + } + + if (isTablet) { + return { + maxRatio: 16 / 9, + minRatio: 2 / 3, + bigMaxRatio: 16 / 9, + bigMinRatio: 9 / 16 + }; + } + + return { + maxRatio: 16 / 9, + minRatio: 9 / 16, + bigMaxRatio: 16 / 9, + bigMinRatio: 9 / 16 + }; + } + + protected getResponsivePercentages() { + const isMobile = this.viewportSrv.isMobile(); + const isTablet = this.viewportSrv.isTablet(); + const isPortrait = this.viewportSrv.isPortrait(); + + if (isMobile && isPortrait) { + return { + bigPercentage: 0.85, + minBigPercentage: 0.7 + }; + } + + if (isMobile) { + return { + bigPercentage: 0.82, + minBigPercentage: 0.65 + }; + } + + if (isTablet && isPortrait) { + return { + bigPercentage: 0.83, + minBigPercentage: 0.6 + }; + } + + if (isTablet) { + return { + bigPercentage: 0.81, + minBigPercentage: 0.55 + }; + } + + return { + bigPercentage: 0.8, + minBigPercentage: 0.5 + }; + } + + protected setupViewportListener(): void { + effect(() => { + const viewportInfo = this.viewportSrv.viewportInfo(); + const isMobile = this.viewportSrv.isMobile(); + const orientation = this.viewportSrv.orientation(); + this.updateLayoutOptions(); + }); + } + + protected updateLayoutOptions(): void { + const newOptions = this.getOptions(); + + if (this.hasSignificantChanges(this.openviduLayoutOptions, newOptions)) { + this.openviduLayoutOptions = newOptions; + + if (this.openviduLayout && this.layoutContainer) { + this.openviduLayout.updateLayout(this.layoutContainer, this.openviduLayoutOptions); + this.sendLayoutWidthEvent(); + } + } + } + + protected hasSignificantChanges(oldOptions: OpenViduLayoutOptions, newOptions: OpenViduLayoutOptions): boolean { + if (!oldOptions) return true; + + const significantProps: (keyof OpenViduLayoutOptions)[] = [ + 'maxRatio', + 'minRatio', + 'bigMaxRatio', + 'bigMinRatio', + 'bigPercentage', + 'alignItems', + 'bigAlignItems' + ]; + + return significantProps.some( + (prop) => Math.abs((oldOptions[prop] as number) - (newOptions[prop] as number)) > 0.01 || oldOptions[prop] !== newOptions[prop] + ); } protected sendLayoutWidthEvent() {