I have a mat-table where each row represents a audio recording for a call. At the end of each row, there is a play button which plays the respective audio file when clicked. However, when I click any play button, the correct audio file plays but all the play buttons change to stop buttons. Here is a snippet of the html:
<!-- audio display -->
<ng-container matColumnDef="isPlaying">
<mat-header-cell *matHeaderCellDef>Playing</mat-header-cell>
<mat-cell *matCellDef="let recording">
<button mat-button (click)="playToggle(recording)" [disabled]="state?.error" *ngIf="!state?.playing">
<mat-icon mat-list-icon>play_arrow</mat-icon>
</button>
<button mat-button (click)="pause()" *ngIf="state?.playing">
<mat-icon mat-list-icon>pause</mat-icon>
</button>
</mat-cell>
</ng-container>
recordings.component.ts
@Component({
selector: 'recordings',
templateUrl: './recordings.component.html',
styleUrls: ['./recordings.component.scss'],
encapsulation: ViewEncapsulation.None,
animations: fuseAnimations
})
export class RecordingsComponent implements OnInit {
// Class Variables
recordingPlaying = false;
buttonValue = 'Play Recording';
displayColumns: string[] = ['date', 'time'];
dataSource: MatTableDataSource < Recording > ;
currentUser: BehaviorSubject < UserModel > ;
dataLoading = true;
recordings: Recording[];
recording: Recording;
state: RecordingStreamState;
currentFile: any = {};
@ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort;
@Input()
telephone: string;
// Private
private audio = new Audio();
private _unsubscribeAll: Subject < any > ;
constructor(
private _recordingsService: RecordingsService,
private _usersService: UserService,
private _matSnackBar: MatSnackBar,
private _fuseSidebarService: FuseSidebarService,
private _matDialog: MatDialog
) {
// Set the defaults
this._unsubscribeAll = new Subject();
}
ngOnInit(): void {
this.currentUser = this._usersService.user;
this.loadRecordings();
// listen to stream state
this._recordingsService.getState().subscribe(state => {
this.state = state;
console.log('this.state: ' JSON.stringify(this.state));
});
}
public loadRecordings(): void {
this._recordingsService.getRecordings(this.telephone).then(data => {
this.recordings = data;
});
this._recordingsService.onRecordingClick
.subscribe((recordings: Recording[]) => this.handleRecordings(recordings),
err => this.handleRecordingsError(err)
);
}
private handleRecordings(recordings: Recording[]): void {
this.dataLoading = false;
this.dataSource = new MatTableDataSource(recordings);
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
}
private handleRecordingsError(err): void {
console.error(err);
// this.alertService.error("Problem loading contacts!");
}
playToggle(recording): void {
// TODO: (remove) if this is the reocrding already playing, pause, else play
if (recording === this.recording) {
this.pause();
this.recording = null;
} else {
this.recording = recording;
console.log('this.recording: ' JSON.stringify(this.recording));
if (this.recording === undefined) {
console.log('could not find recording.');
} else {
this.playStream(this.recording.url);
}
}
}
playStream(url): void {
// subscribes to playStream in our service and starts listening to media events
// like canplay, playing etc. This should be done in the stateChange object
// but we use this method to start the observable and audio playback
this._recordingsService.playStream(url).subscribe(events => {
});
}
pause(): void {
this._recordingsService.pause();
}
play(): void {
this._recordingsService.play();
}
stop(): void {
this._recordingsService.stop();
}
}
recordings.service.ts
@Injectable()
export class RecordingsService implements Resolve < any > {
// Class variables
routeParams: any;
recordings: Recording[];
onRecordingClick: BehaviorSubject < any > ;
private stop$ = new Subject();
private audioObj = new Audio();
audioEvents = [
'ended',
'error',
'play',
'playing',
'pause',
'timeupdate',
'canplay',
'loadedmetadata',
'loadstart',
];
// gets from the interface we created
private state: RecordingStreamState = {
playing: false,
readableCurrentTime: '',
readableDuration: '',
duration: undefined,
currentTime: undefined,
canplay: false,
error: false,
};
private stateChange: BehaviorSubject < RecordingStreamState > = new BehaviorSubject(
this.state
);
// tslint:disable-next-line:typedef
private streamObservable(url) {
console.log('in streamObservable in service');
// tslint:disable-next-line:no-unused-expression
return new Observable(observer => {
// Play audio
this.audioObj.src = url;
this.audioObj.load();
this.audioObj.play();
// a way for the observer to react to items the observable emits
const handler = (event: Event) => {
this.updateStateEvents(event);
observer.next(event);
};
this.addEvents(this.audioObj, this.audioEvents, handler);
return () => {
// stop playing
this.audioObj.pause();
this.audioObj.currentTime = 0;
// remove event listeners
this.removeEvents(this.audioObj, this.audioEvents, handler);
};
});
}
private addEvents(obj, events, handler): void {
events.forEach(event => {
obj.addEventListener(event, handler);
});
}
private removeEvents(obj, events, handler): void {
events.forEach(event => {
obj.removeEventListener(event, handler);
});
}
// how to handle the different events , recording: Recording
private updateStateEvents(event: Event): void {
console.log('event_type: ' event.type);
switch (event.type) {
case 'canplay':
this.state.duration = this.audioObj.duration;
this.state.readableDuration = this.formatTime(this.state.duration);
this.state.canplay = true;
break;
case 'playing':
this.state.playing = true;
break;
case 'pause':
this.state.playing = false;
break;
case 'timeupdate':
this.state.currentTime = this.audioObj.currentTime;
this.state.readableCurrentTime = this.formatTime(
this.state.currentTime
);
break;
case 'error':
this.resetState();
this.state.error = true;
break;
}
this.stateChange.next(this.state);
}
private resetState(): void {
this.state = {
playing: false,
readableCurrentTime: '',
readableDuration: '',
duration: undefined,
currentTime: undefined,
canplay: false,
error: false,
};
}
getState(): Observable < RecordingStreamState > {
return this.stateChange.asObservable();
}
playStream(url): Observable < any > {
return this.streamObservable(url).pipe(takeUntil(this.stop$));
}
play(): void {
this.audioObj.play();
}
pause(): void {
this.audioObj.pause();
}
stop(): void {
this.stop$.next();
}
seekTo(seconds): void {
this.audioObj.currentTime = seconds;
}
formatTime(time: number, format: string = 'HH:mm:ss'): string {
const momentTime = time * 1000;
return moment.utc(momentTime).format(format);
}
/**
* Constructor
*
* @param {HttpClient} _httpClient
*/
constructor(
private _httpClient: HttpClient,
) {
// Set the defaults
this.onRecordingClick = new BehaviorSubject([]);
}
}
CodePudding user response:
Looking at your code I can think of a probable solution. I don't know how the Recording
object is defined, but if it contains some id
it might work. Or you can use some other identifier.
As I said in the comment, state
property is shared by all entries within your table. So it's not enough to determine the current playing record. Once it's true for one it's true for all of them. But maybe this can help:
<!-- audio display -->
<ng-container matColumnDef="isPlaying">
<mat-header-cell *matHeaderCellDef>Playing</mat-header-cell>
<mat-cell *matCellDef="let rowRecording">
<button mat-button (click)="playToggle(rowRecording)" [disabled]="state?.error" *ngIf="!state?.playing">
<mat-icon mat-list-icon>play_arrow</mat-icon>
</button>
<button mat-button (click)="pause()" *ngIf="state?.playing && rowRecording.id === recording.id">
<mat-icon mat-list-icon>pause</mat-icon>
</button>
</mat-cell>
</ng-container>
As you can see, I renamed the recording
within HTML to rowRecording
to differentiate it from the one from the .ts code. Then I'm checking if the state is playing but also if the rowRecording.id
equals the ID of assigned recording within .ts. You do it in the playToggle
function.
But I would recommend extending the RecordingStreamState
object with PlayingRecordId
property which would be assigned when you press play. Then you can do someting like this (using your version of HTML)
<button
mat-button
(click)="pause()"
*ngIf="state?.playing && state?.playingRecordId === recording.id"
>