UNPKG

@epicgames-ps/lib-pixelstreamingfrontend-ue5.4

Version:
333 lines (302 loc) 12.2 kB
// Copyright Epic Games, Inc. All Rights Reserved. import { InboundRTPStats, InboundVideoStats, InboundAudioStats } from './InboundRTPStats'; import { InboundTrackStats } from './InboundTrackStats'; import { DataChannelStats } from './DataChannelStats'; import { CandidateStat } from './CandidateStat'; import { CandidatePairStats } from './CandidatePairStats'; import { OutBoundRTPStats, OutBoundVideoStats } from './OutBoundRTPStats'; import { SessionStats } from './SessionStats'; import { StreamStats } from './StreamStats'; import { CodecStats } from './CodecStats'; import { Logger } from '../Logger/Logger'; /** * The Aggregated Stats that is generated from the RTC Stats Report */ type RTCStatsTypePS = RTCStatsType | 'stream' | 'media-playout' | 'track'; export class AggregatedStats { inboundVideoStats: InboundVideoStats; inboundAudioStats: InboundAudioStats; lastVideoStats: InboundVideoStats; lastAudioStats: InboundAudioStats; candidatePairs: Array<CandidatePairStats>; DataChannelStats: DataChannelStats; localCandidates: Array<CandidateStat>; remoteCandidates: Array<CandidateStat>; outBoundVideoStats: OutBoundVideoStats; sessionStats: SessionStats; streamStats: StreamStats; codecs: Map<string, string>; transportStats: RTCTransportStats; constructor() { this.inboundVideoStats = new InboundVideoStats(); this.inboundAudioStats = new InboundAudioStats(); this.DataChannelStats = new DataChannelStats(); this.outBoundVideoStats = new OutBoundVideoStats(); this.sessionStats = new SessionStats(); this.streamStats = new StreamStats(); this.codecs = new Map<string, string>(); } /** * Gather all the information from the RTC Peer Connection Report * @param rtcStatsReport - RTC Stats Report */ processStats(rtcStatsReport: RTCStatsReport) { this.localCandidates = new Array<CandidateStat>(); this.remoteCandidates = new Array<CandidateStat>(); this.candidatePairs = new Array<CandidatePairStats>(); rtcStatsReport.forEach((stat) => { const type: RTCStatsTypePS = stat.type; switch (type) { case 'candidate-pair': this.handleCandidatePair(stat); break; case 'certificate': break; case 'codec': this.handleCodec(stat); break; case 'data-channel': this.handleDataChannel(stat); break; case 'inbound-rtp': this.handleInBoundRTP(stat); break; case 'local-candidate': this.handleLocalCandidate(stat); break; case 'media-source': break; case 'media-playout': break; case 'outbound-rtp': break; case 'peer-connection': break; case 'remote-candidate': this.handleRemoteCandidate(stat); break; case 'remote-inbound-rtp': break; case 'remote-outbound-rtp': this.handleRemoteOutBound(stat); break; case 'track': this.handleTrack(stat); break; case 'transport': this.handleTransport(stat); break; case 'stream': this.handleStream(stat); break; default: Logger.Error(Logger.GetStackTrace(), 'unhandled Stat Type'); Logger.Log(Logger.GetStackTrace(), stat); break; } }); } /** * Process stream stats data from webrtc * * @param stat - the stats coming in from webrtc */ handleStream(stat: StreamStats) { this.streamStats = stat; } /** * Process the Ice Candidate Pair Data * @param stat - the stats coming in from ice candidates */ handleCandidatePair(stat: CandidatePairStats) { // Add the candidate pair to the candidate pair array this.candidatePairs.push(stat) } /** * Process the Data Channel Data * @param stat - the stats coming in from the data channel */ handleDataChannel(stat: DataChannelStats) { this.DataChannelStats.bytesReceived = stat.bytesReceived; this.DataChannelStats.bytesSent = stat.bytesSent; this.DataChannelStats.dataChannelIdentifier = stat.dataChannelIdentifier; this.DataChannelStats.id = stat.id; this.DataChannelStats.label = stat.label; this.DataChannelStats.messagesReceived = stat.messagesReceived; this.DataChannelStats.messagesSent = stat.messagesSent; this.DataChannelStats.protocol = stat.protocol; this.DataChannelStats.state = stat.state; this.DataChannelStats.timestamp = stat.timestamp; } /** * Process the Local Ice Candidate Data * @param stat - local stats */ handleLocalCandidate(stat: CandidateStat) { const localCandidate = new CandidateStat(); localCandidate.label = 'local-candidate'; localCandidate.address = stat.address; localCandidate.port = stat.port; localCandidate.protocol = stat.protocol; localCandidate.candidateType = stat.candidateType; localCandidate.id = stat.id; localCandidate.relayProtocol = stat.relayProtocol; localCandidate.transportId = stat.transportId; this.localCandidates.push(localCandidate); } /** * Process the Remote Ice Candidate Data * @param stat - ice candidate stats */ handleRemoteCandidate(stat: CandidateStat) { const RemoteCandidate = new CandidateStat(); RemoteCandidate.label = 'remote-candidate'; RemoteCandidate.address = stat.address; RemoteCandidate.port = stat.port; RemoteCandidate.protocol = stat.protocol; RemoteCandidate.id = stat.id; RemoteCandidate.candidateType = stat.candidateType; RemoteCandidate.relayProtocol = stat.relayProtocol; RemoteCandidate.transportId = stat.transportId this.remoteCandidates.push(RemoteCandidate); } /** * Process the Inbound RTP Audio and Video Data * @param stat - inbound rtp stats */ handleInBoundRTP(stat: InboundRTPStats) { switch (stat.kind) { case 'video': // Need to convert to unknown first to remove an error around // InboundVideoStats having the bitrate member which isn't found on // the InboundRTPStats this.inboundVideoStats = stat as unknown as InboundVideoStats; if (this.lastVideoStats != undefined) { this.inboundVideoStats.bitrate = (8 * (this.inboundVideoStats.bytesReceived - this.lastVideoStats.bytesReceived)) / (this.inboundVideoStats.timestamp - this.lastVideoStats.timestamp); this.inboundVideoStats.bitrate = Math.floor( this.inboundVideoStats.bitrate ); } this.lastVideoStats = { ...this.inboundVideoStats }; break; case 'audio': // Need to convert to unknown first to remove an error around // InboundAudioStats having the bitrate member which isn't found on // the InboundRTPStats this.inboundAudioStats = stat as unknown as InboundAudioStats; if (this.lastAudioStats != undefined) { this.inboundAudioStats.bitrate = (8 * (this.inboundAudioStats.bytesReceived - this.lastAudioStats.bytesReceived)) / (this.inboundAudioStats.timestamp - this.lastAudioStats.timestamp); this.inboundAudioStats.bitrate = Math.floor( this.inboundAudioStats.bitrate ); } this.lastAudioStats = { ...this.inboundAudioStats }; break; default: Logger.Log(Logger.GetStackTrace(), 'Kind is not handled'); break; } } /** * Process the outbound RTP Audio and Video Data * @param stat - remote outbound stats */ handleRemoteOutBound(stat: OutBoundRTPStats) { switch (stat.kind) { case 'video': this.outBoundVideoStats.bytesSent = stat.bytesSent; this.outBoundVideoStats.id = stat.id; this.outBoundVideoStats.localId = stat.localId; this.outBoundVideoStats.packetsSent = stat.packetsSent; this.outBoundVideoStats.remoteTimestamp = stat.remoteTimestamp; this.outBoundVideoStats.timestamp = stat.timestamp; break; case 'audio': break; default: break; } } /** * Process the Inbound Video Track Data * @param stat - video track stats */ handleTrack(stat: InboundTrackStats) { // we only want to extract stats from the video track if ( stat.type === 'track' && (stat.trackIdentifier === 'video_label' || stat.kind === 'video') ) { this.inboundVideoStats.framesDropped = stat.framesDropped; this.inboundVideoStats.framesReceived = stat.framesReceived; this.inboundVideoStats.frameHeight = stat.frameHeight; this.inboundVideoStats.frameWidth = stat.frameWidth; } } handleTransport(stat: RTCTransportStats){ this.transportStats = stat; } handleCodec(stat: CodecStats) { const codecId = stat.id; const codecType = `${stat.mimeType .replace('video/', '') .replace('audio/', '')}${ stat.sdpFmtpLine ? ` ${stat.sdpFmtpLine}` : '' }`; this.codecs.set(codecId, codecType); } handleSessionStatistics( videoStartTime: number, inputController: boolean | null, videoEncoderAvgQP: number ) { const deltaTime = Date.now() - videoStartTime; this.sessionStats.runTime = new Date(deltaTime) .toISOString() .substr(11, 8) .toString(); const controlsStreamInput = inputController === null ? 'Not sent yet' : inputController ? 'true' : 'false'; this.sessionStats.controlsStreamInput = controlsStreamInput; this.sessionStats.videoEncoderAvgQP = videoEncoderAvgQP; } /** * Check if a value coming in from our stats is actually a number * @param value - the number to be checked */ isNumber(value: unknown): boolean { return typeof value === 'number' && isFinite(value); } /** * Helper function to return the active candidate pair * @returns The candidate pair that is currently receiving data */ public getActiveCandidatePair(): CandidatePairStats | null { // Check if the RTCTransport stat is not undefined if (this.transportStats){ // Return the candidate pair that matches the transport candidate pair id return this.candidatePairs.find((candidatePair) => candidatePair.id === this.transportStats.selectedCandidatePairId, null); } // Fall back to the selected candidate pair return this.candidatePairs.find((candidatePair) => candidatePair.selected, null); } }