frontend: enhance view recording component with error handling and status messages

This commit is contained in:
juancarmore 2025-06-17 15:11:49 +02:00
parent 253c7cb37b
commit fdc93e4f19
4 changed files with 111 additions and 29 deletions

View File

@ -108,7 +108,9 @@ export class RoomRecordingsComponent implements OnInit {
openShareDialog(recording: MeetRecordingInfo) { openShareDialog(recording: MeetRecordingInfo) {
this.dialog.open(ShareRecordingDialogComponent, { this.dialog.open(ShareRecordingDialogComponent, {
width: '400px', width: '400px',
data: { recordingId: recording.recordingId } data: {
recordingId: recording.recordingId
}
}); });
} }

View File

@ -1,25 +1,58 @@
@if (recording && recordingUrl) { @if (recording) {
<mat-card class="recording-container"> <mat-card class="recording-container">
<div class="video-container"> @if (recordingUrl) {
<video controls class="video-player"> @if (!videoError) {
<source [src]="recordingUrl" type="video/mp4" /> <div class="video-container">
Your browser does not support the video tag. <video
</video> autoplay
</div> controls
muted
class="video-player"
[src]="recordingUrl"
(error)="videoError = true"
>
Your browser does not support the video tag.
</video>
</div>
} @else {
<div class="video-error">
<mat-icon color="warn">error</mat-icon>
<span>Error loading video. Please try again later.</span>
</div>
}
} @else {
@if (['STARTING', 'ACTIVE', 'ENDING'].includes(recording.status)) {
<div class="status-message">
<mat-icon color="accent">hourglass_empty</mat-icon>
<span>Recording is still in progress...</span>
</div>
} @else {
<div class="status-message error">
<mat-icon color="warn">error_outline</mat-icon>
<span>Recording has failed</span>
</div>
}
}
<mat-card-content> <mat-card-content>
<h2 class="title">{{ recording.roomId }}</h2> <h2 class="title">{{ recording.roomId }}</h2>
<div class="info-actions"> <div class="info-actions">
<span class="date">{{ recording.startDate | date: 'M/d/yy, H:mm' }}</span> <span class="date">{{ recording.startDate | date: 'M/d/yy, H:mm' }}</span>
<div class="actions"> @if (recordingUrl && !videoError) {
<button mat-icon-button color="primary" (click)="downloadRecording()" aria-label="Download"> <div class="actions">
<mat-icon>download</mat-icon> <button mat-icon-button color="primary" (click)="downloadRecording()" aria-label="Download">
</button> <mat-icon>download</mat-icon>
<button mat-icon-button color="accent" (click)="openShareDialog()" aria-label="Share"> </button>
<mat-icon>share</mat-icon> <button mat-icon-button color="accent" (click)="openShareDialog()" aria-label="Share">
</button> <mat-icon>share</mat-icon>
</div> </button>
</div>
}
</div> </div>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
} @else {
<div class="loader-container">
<mat-spinner></mat-spinner>
</div>
} }

View File

@ -1,3 +1,10 @@
.loader-container {
display: flex;
justify-content: center;
align-items: center;
height: 60vh;
}
.recording-container { .recording-container {
max-width: 900px; max-width: 900px;
margin: 20px auto; margin: 20px auto;
@ -18,6 +25,31 @@
height: 100%; height: 100%;
} }
.status-message {
display: flex;
align-items: center;
gap: 8px;
color: #555;
font-size: 1.1rem;
padding: 20px;
}
.status-message.error {
color: #d32f2f;
}
.video-error {
display: flex;
align-items: center;
gap: 8px;
padding: 16px;
background-color: #fbe9e7;
border: 1px solid #ef9a9a;
color: #c62828;
border-radius: 4px;
margin-bottom: 10px;
}
.title { .title {
font-size: 1.8rem; font-size: 1.8rem;
font-weight: 600; font-weight: 600;

View File

@ -4,25 +4,29 @@ import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { ActivatedRoute } from '@angular/router'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { ActivatedRoute, Router } from '@angular/router';
import { ShareRecordingDialogComponent } from '@lib/components'; import { ShareRecordingDialogComponent } from '@lib/components';
import { HttpService } from '@lib/services'; import { HttpService } from '@lib/services';
import { ActionService, MeetRecordingInfo } from 'shared-meet-components'; import { ActionService, MeetRecordingInfo, MeetRecordingStatus } from 'shared-meet-components';
@Component({ @Component({
selector: 'app-view-recording', selector: 'app-view-recording',
templateUrl: './view-recording.component.html', templateUrl: './view-recording.component.html',
styleUrls: ['./view-recording.component.scss'], styleUrls: ['./view-recording.component.scss'],
standalone: true, standalone: true,
imports: [MatCardModule, MatButtonModule, MatIconModule, DatePipe] imports: [MatCardModule, MatButtonModule, MatIconModule, DatePipe, MatProgressSpinnerModule]
}) })
export class ViewRecordingComponent implements OnInit { export class ViewRecordingComponent implements OnInit {
recording: MeetRecordingInfo | undefined; recording?: MeetRecordingInfo;
recordingUrl: string | undefined; recordingUrl?: string;
videoError = false;
constructor( constructor(
protected httpService: HttpService, protected httpService: HttpService,
protected actionService: ActionService, protected actionService: ActionService,
protected router: Router,
protected route: ActivatedRoute, protected route: ActivatedRoute,
protected dialog: MatDialog protected dialog: MatDialog
) {} ) {}
@ -31,18 +35,26 @@ export class ViewRecordingComponent implements OnInit {
const recordingId = this.route.snapshot.paramMap.get('recording-id'); const recordingId = this.route.snapshot.paramMap.get('recording-id');
const secret = this.route.snapshot.queryParams['secret']; const secret = this.route.snapshot.queryParams['secret'];
this.recording = await this.httpService.getRecording(recordingId!, secret!); try {
this.playRecording(); this.recording = await this.httpService.getRecording(recordingId!, secret!);
}
playRecording() { if (this.recording.status === MeetRecordingStatus.COMPLETE) {
this.recordingUrl = this.httpService.getRecordingMediaUrl(this.recording!.recordingId); this.recordingUrl = this.httpService.getRecordingMediaUrl(this.recording!.recordingId);
}
} catch (error) {
console.error('Error fetching recording:', error);
}
} }
downloadRecording() { downloadRecording() {
if (!this.recording || !this.recordingUrl) {
console.error('Recording is not available for download');
return;
}
const link = document.createElement('a'); const link = document.createElement('a');
link.href = this.recordingUrl!; link.href = this.recordingUrl;
link.download = this.recording!.filename || 'openvidu-recording.mp4'; link.download = this.recording.filename || 'openvidu-recording.mp4';
link.dispatchEvent( link.dispatchEvent(
new MouseEvent('click', { new MouseEvent('click', {
bubbles: true, bubbles: true,
@ -58,7 +70,10 @@ export class ViewRecordingComponent implements OnInit {
openShareDialog() { openShareDialog() {
this.dialog.open(ShareRecordingDialogComponent, { this.dialog.open(ShareRecordingDialogComponent, {
width: '400px', width: '400px',
data: { recordingId: this.recording!.recordingId } data: {
recordingId: this.recording!.recordingId,
recordingUrl: window.location.href
}
}); });
} }
} }