ov-components: enhance screenshare button functionality for Firefox

- Updated the screenshare button in the toolbar to behave differently based on the browser type (Firefox vs others).
- Added an input property `isFirefoxBrowser` to the `ToolbarMediaButtonsComponent` to determine the browser type.
- Modified the HTML template to conditionally render the screenshare button and its menu based on the browser.
- Improved the `switchScreenshare` method in the `ParticipantService` to handle errors more gracefully and ensure the local participant is defined before proceeding.
- Refactored the `switchScreenshare` method in the `ParticipantModel` to reject promises with meaningful messages when no active screen share track is found.
master
CSantosM 2026-03-16 13:15:13 +01:00
parent 87b3ac3716
commit 34e34ee078
7 changed files with 2678 additions and 1541 deletions

File diff suppressed because it is too large Load Diff

View File

@ -36,31 +36,45 @@
<!-- Screenshare button - Only visible on tablet+ or when active --> <!-- Screenshare button - Only visible on tablet+ or when active -->
@if (showScreenshareButtonDirect()) { @if (showScreenshareButtonDirect()) {
<button @if (isFirefoxBrowser) {
mat-icon-button <button
id="screenshare-btn" mat-icon-button
[matMenuTriggerFor]="isScreenShareEnabled ? screenshareMenu : null" id="screenshare-btn"
[disabled]="isConnectionLost" [disabled]="isConnectionLost"
[class.active-btn]="isScreenShareEnabled" [class.active-btn]="isScreenShareEnabled"
[class.mobile-btn]="isMobileView()" [class.mobile-btn]="isMobileView()"
matTooltip="{{ isScreenShareEnabled ? ('TOOLBAR.DISABLE_SCREEN' | translate) : ('TOOLBAR.ENABLE_SCREEN' | translate) }}" matTooltip="{{ isScreenShareEnabled ? ('TOOLBAR.DISABLE_SCREEN' | translate) : ('TOOLBAR.ENABLE_SCREEN' | translate) }}"
(click)="!isScreenShareEnabled && onScreenShareToggle()" (click)="onScreenShareToggle()"
> >
<mat-icon>screen_share</mat-icon> <mat-icon>screen_share</mat-icon>
</button> </button>
} @else {
<button
mat-icon-button
id="screenshare-btn"
[matMenuTriggerFor]="isScreenShareEnabled ? screenshareMenu : null"
[disabled]="isConnectionLost"
[class.active-btn]="isScreenShareEnabled"
[class.mobile-btn]="isMobileView()"
matTooltip="{{ isScreenShareEnabled ? ('TOOLBAR.DISABLE_SCREEN' | translate) : ('TOOLBAR.ENABLE_SCREEN' | translate) }}"
(click)="!isScreenShareEnabled && onScreenShareToggle()"
>
<mat-icon>screen_share</mat-icon>
</button>
<!-- Screenshare button menu --> <!-- Screenshare button menu -->
<mat-menu #screenshareMenu="matMenu" id="screenshare-menu"> <mat-menu #screenshareMenu="matMenu" id="screenshare-menu">
<button mat-menu-item (click)="onScreenTrackReplace()" id="replace-screen-button"> <button mat-menu-item (click)="onScreenTrackReplace()" id="replace-screen-button">
<mat-icon>picture_in_picture</mat-icon> <mat-icon>picture_in_picture</mat-icon>
<span>{{ 'STREAM.REPLACE_SCREEN' | translate }}</span> <span>{{ 'STREAM.REPLACE_SCREEN' | translate }}</span>
</button> </button>
<mat-divider class="divider"></mat-divider> <mat-divider class="divider"></mat-divider>
<button mat-menu-item (click)="onScreenShareToggle()" id="disable-screen-button"> <button mat-menu-item (click)="onScreenShareToggle()" id="disable-screen-button">
<mat-icon>stop_screen_share</mat-icon> <mat-icon>stop_screen_share</mat-icon>
<span>{{ 'TOOLBAR.DISABLE_SCREEN' | translate }}</span> <span>{{ 'TOOLBAR.DISABLE_SCREEN' | translate }}</span>
</button> </button>
</mat-menu> </mat-menu>
}
} }
<!-- Additional buttons injection from parent component (desktop/tablet only) --> <!-- Additional buttons injection from parent component (desktop/tablet only) -->

View File

@ -28,6 +28,7 @@ export class ToolbarMediaButtonsComponent {
// Screenshare related inputs // Screenshare related inputs
@Input() showScreenshareButton: boolean = true; @Input() showScreenshareButton: boolean = true;
@Input() isScreenShareEnabled: boolean = false; @Input() isScreenShareEnabled: boolean = false;
@Input() isFirefoxBrowser: boolean = false;
// Device availability inputs // Device availability inputs
@Input() hasVideoDevices: boolean = true; @Input() hasVideoDevices: boolean = true;

View File

@ -49,6 +49,7 @@
[microphoneMuteChanging]="microphoneMuteChanging" [microphoneMuteChanging]="microphoneMuteChanging"
[showScreenshareButton]="showScreenshareButton" [showScreenshareButton]="showScreenshareButton"
[isScreenShareEnabled]="isScreenShareEnabled" [isScreenShareEnabled]="isScreenShareEnabled"
[isFirefoxBrowser]="isFirefoxBrowser"
[hasVideoDevices]="hasVideoDevices" [hasVideoDevices]="hasVideoDevices"
[hasAudioDevices]="hasAudioDevices" [hasAudioDevices]="hasAudioDevices"
[isConnectionLost]="isConnectionLost" [isConnectionLost]="isConnectionLost"

View File

@ -344,6 +344,11 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
*/ */
showCaptionsButton: boolean = true; showCaptionsButton: boolean = true;
/**
* @internal
*/
isFirefoxBrowser: boolean = false;
/** /**
* @ignore * @ignore
*/ */
@ -450,6 +455,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
private templateManagerService: TemplateManagerService private templateManagerService: TemplateManagerService
) { ) {
this.log = this.loggerSrv.get('ToolbarComponent'); this.log = this.loggerSrv.get('ToolbarComponent');
this.isFirefoxBrowser = this.platformService.isFirefox();
// Effect to react to local participant changes // Effect to react to local participant changes
effect(() => { effect(() => {

View File

@ -436,14 +436,19 @@ export class ParticipantModel {
* @internal * @internal
*/ */
async switchScreenshare(newTrack: LocalTrack): Promise<void> { async switchScreenshare(newTrack: LocalTrack): Promise<void> {
if (this.participant instanceof LocalParticipant) { if (!(this.participant instanceof LocalParticipant)) {
const screenTrack = this.tracks.find((track) => track.source === Track.Source.ScreenShare);
if (screenTrack) {
await (screenTrack.videoTrack as LocalTrack).replaceTrack(newTrack.mediaStreamTrack);
return Promise.resolve();
}
return Promise.reject("Remote participant can't switch screen share"); return Promise.reject("Remote participant can't switch screen share");
} }
const screenTrack = this.tracks.find((track) => track.source === Track.Source.ScreenShare);
if (!screenTrack || !screenTrack.videoTrack) {
return Promise.reject('No active screen share track to switch');
}
const currentTrack = screenTrack.videoTrack as LocalTrack;
await currentTrack.replaceTrack(newTrack.mediaStreamTrack);
return Promise.resolve();
} }
/** /**

View File

@ -189,19 +189,26 @@ export class ParticipantService {
* Switches the active screen share track showing a native browser dialog to select a screen or window. * Switches the active screen share track showing a native browser dialog to select a screen or window.
*/ */
async switchScreenShare(): Promise<void> { async switchScreenShare(): Promise<void> {
if (this.localParticipant) { if (!this.localParticipant) {
const options = this.getScreenCaptureOptions();
const [newTrack] = await this.localParticipant.createScreenTracks(options);
if (newTrack) {
newTrack?.addListener('ended', async () => {
this.log.d('Clicked native stop button. Stopping screen sharing');
await this.setScreenShareEnabled(false);
});
await this.localParticipant.switchScreenshare(newTrack);
}
} else {
this.log.e('Local participant is undefined when switching screenshare'); this.log.e('Local participant is undefined when switching screenshare');
return;
}
// Chrome / Safari: seamless replaceTrack keeps the same publication SID.
const options = this.getScreenCaptureOptions();
const [newTrack] = await this.localParticipant.createScreenTracks(options);
if (newTrack) {
newTrack.addListener('ended', async () => {
this.log.d('Clicked native stop button. Stopping screen sharing');
await this.setScreenShareEnabled(false);
});
try {
await this.localParticipant.switchScreenshare(newTrack);
} catch (error) {
newTrack.stop();
throw error;
}
} }
// this.updateLocalParticipant(); // this.updateLocalParticipant();
@ -459,7 +466,6 @@ export class ParticipantService {
} }
} }
/** /**
* Returns the participant with the given identity. * Returns the participant with the given identity.
* @param identity * @param identity