github.com/pion/webrtc/v4@v4.0.1/stats_go_test.go (about)

     1  // SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
     2  // SPDX-License-Identifier: MIT
     3  
     4  //go:build !js
     5  // +build !js
     6  
     7  package webrtc
     8  
     9  import (
    10  	"encoding/json"
    11  	"fmt"
    12  	"sync"
    13  	"testing"
    14  	"time"
    15  
    16  	"github.com/pion/ice/v4"
    17  	"github.com/stretchr/testify/assert"
    18  	"github.com/stretchr/testify/require"
    19  )
    20  
    21  var errReceiveOfferTimeout = fmt.Errorf("timed out waiting to receive offer")
    22  
    23  func TestStatsTimestampTime(t *testing.T) {
    24  	for _, test := range []struct {
    25  		Timestamp StatsTimestamp
    26  		WantTime  time.Time
    27  	}{
    28  		{
    29  			Timestamp: 0,
    30  			WantTime:  time.Unix(0, 0),
    31  		},
    32  		{
    33  			Timestamp: 1,
    34  			WantTime:  time.Unix(0, 1e6),
    35  		},
    36  		{
    37  			Timestamp: 0.001,
    38  			WantTime:  time.Unix(0, 1e3),
    39  		},
    40  	} {
    41  		if got, want := test.Timestamp.Time(), test.WantTime.UTC(); got != want {
    42  			t.Fatalf("StatsTimestamp(%v).Time() = %v, want %v", test.Timestamp, got, want)
    43  		}
    44  	}
    45  }
    46  
    47  type statSample struct {
    48  	name  string
    49  	stats Stats
    50  	json  string
    51  }
    52  
    53  func getStatsSamples() []statSample {
    54  	codecStats := CodecStats{
    55  		Timestamp:      1688978831527.718,
    56  		Type:           StatsTypeCodec,
    57  		ID:             "COT01_111_minptime=10;useinbandfec=1",
    58  		PayloadType:    111,
    59  		CodecType:      CodecTypeEncode,
    60  		TransportID:    "T01",
    61  		MimeType:       "audio/opus",
    62  		ClockRate:      48000,
    63  		Channels:       2,
    64  		SDPFmtpLine:    "minptime=10;useinbandfec=1",
    65  		Implementation: "libvpx",
    66  	}
    67  	codecStatsJSON := `
    68  {
    69  	"timestamp": 1688978831527.718,
    70  	"type": "codec",
    71  	"id": "COT01_111_minptime=10;useinbandfec=1",
    72  	"payloadType": 111,
    73  	"codecType": "encode",
    74  	"transportId": "T01",
    75  	"mimeType": "audio/opus",
    76  	"clockRate": 48000,
    77  	"channels": 2,
    78  	"sdpFmtpLine": "minptime=10;useinbandfec=1",
    79  	"implementation": "libvpx"
    80  }
    81  `
    82  	inboundRTPStreamStats := InboundRTPStreamStats{
    83  		Mid:                            "1",
    84  		Timestamp:                      1688978831527.718,
    85  		ID:                             "IT01A2184088143",
    86  		Type:                           StatsTypeInboundRTP,
    87  		SSRC:                           2184088143,
    88  		Kind:                           "audio",
    89  		TransportID:                    "T01",
    90  		CodecID:                        "CIT01_111_minptime=10;useinbandfec=1",
    91  		FIRCount:                       1,
    92  		PLICount:                       2,
    93  		TotalProcessingDelay:           23,
    94  		NACKCount:                      3,
    95  		JitterBufferDelay:              24,
    96  		JitterBufferTargetDelay:        25,
    97  		JitterBufferEmittedCount:       26,
    98  		JitterBufferMinimumDelay:       27,
    99  		TotalSamplesReceived:           28,
   100  		ConcealedSamples:               29,
   101  		SilentConcealedSamples:         30,
   102  		ConcealmentEvents:              31,
   103  		InsertedSamplesForDeceleration: 32,
   104  		RemovedSamplesForAcceleration:  33,
   105  		AudioLevel:                     34,
   106  		TotalAudioEnergy:               35,
   107  		TotalSamplesDuration:           36,
   108  		SLICount:                       4,
   109  		QPSum:                          5,
   110  		TotalDecodeTime:                37,
   111  		TotalInterFrameDelay:           38,
   112  		TotalSquaredInterFrameDelay:    39,
   113  		PacketsReceived:                6,
   114  		PacketsLost:                    7,
   115  		Jitter:                         8,
   116  		PacketsDiscarded:               9,
   117  		PacketsRepaired:                10,
   118  		BurstPacketsLost:               11,
   119  		BurstPacketsDiscarded:          12,
   120  		BurstLossCount:                 13,
   121  		BurstDiscardCount:              14,
   122  		BurstLossRate:                  15,
   123  		BurstDiscardRate:               16,
   124  		GapLossRate:                    17,
   125  		GapDiscardRate:                 18,
   126  		TrackID:                        "d57dbc4b-484b-4b40-9088-d3150e3a2010",
   127  		ReceiverID:                     "R01",
   128  		RemoteID:                       "ROA2184088143",
   129  		FramesDecoded:                  17,
   130  		KeyFramesDecoded:               40,
   131  		FramesRendered:                 41,
   132  		FramesDropped:                  42,
   133  		FrameWidth:                     43,
   134  		FrameHeight:                    44,
   135  		LastPacketReceivedTimestamp:    1689668364374.181,
   136  		HeaderBytesReceived:            45,
   137  		AverageRTCPInterval:            18,
   138  		FECPacketsReceived:             19,
   139  		FECPacketsDiscarded:            46,
   140  		BytesReceived:                  20,
   141  		FramesReceived:                 47,
   142  		PacketsFailedDecryption:        21,
   143  		PacketsDuplicated:              22,
   144  		PerDSCPPacketsReceived: map[string]uint32{
   145  			"123": 23,
   146  		},
   147  	}
   148  	inboundRTPStreamStatsJSON := `
   149  {
   150    "mid": "1",
   151    "timestamp": 1688978831527.718,
   152    "id": "IT01A2184088143",
   153    "type": "inbound-rtp",
   154    "ssrc": 2184088143,
   155    "kind": "audio",
   156    "transportId": "T01",
   157    "codecId": "CIT01_111_minptime=10;useinbandfec=1",
   158    "firCount": 1,
   159    "pliCount": 2,
   160    "totalProcessingDelay": 23,
   161    "nackCount": 3,
   162    "jitterBufferDelay": 24,
   163    "jitterBufferTargetDelay": 25,
   164    "jitterBufferEmittedCount": 26,
   165    "jitterBufferMinimumDelay": 27,
   166    "totalSamplesReceived": 28,
   167    "concealedSamples": 29,
   168    "silentConcealedSamples": 30,
   169    "concealmentEvents": 31,
   170    "insertedSamplesForDeceleration": 32,
   171    "removedSamplesForAcceleration": 33,
   172    "audioLevel": 34,
   173    "totalAudioEnergy": 35,
   174    "totalSamplesDuration": 36,
   175    "sliCount": 4,
   176    "qpSum": 5,
   177    "totalDecodeTime": 37,
   178    "totalInterFrameDelay": 38,
   179    "totalSquaredInterFrameDelay": 39,
   180    "packetsReceived": 6,
   181    "packetsLost": 7,
   182    "jitter": 8,
   183    "packetsDiscarded": 9,
   184    "packetsRepaired": 10,
   185    "burstPacketsLost": 11,
   186    "burstPacketsDiscarded": 12,
   187    "burstLossCount": 13,
   188    "burstDiscardCount": 14,
   189    "burstLossRate": 15,
   190    "burstDiscardRate": 16,
   191    "gapLossRate": 17,
   192    "gapDiscardRate": 18,
   193    "trackId": "d57dbc4b-484b-4b40-9088-d3150e3a2010",
   194    "receiverId": "R01",
   195    "remoteId": "ROA2184088143",
   196    "framesDecoded": 17,
   197    "keyFramesDecoded": 40,
   198    "framesRendered": 41,
   199    "framesDropped": 42,
   200    "frameWidth": 43,
   201    "frameHeight": 44,
   202    "lastPacketReceivedTimestamp": 1689668364374.181,
   203    "headerBytesReceived": 45,
   204    "averageRtcpInterval": 18,
   205    "fecPacketsReceived": 19,
   206    "fecPacketsDiscarded": 46,
   207    "bytesReceived": 20,
   208    "framesReceived": 47,
   209    "packetsFailedDecryption": 21,
   210    "packetsDuplicated": 22,
   211    "perDscpPacketsReceived": {
   212      "123": 23
   213    }
   214  }
   215  `
   216  	outboundRTPStreamStats := OutboundRTPStreamStats{
   217  		Mid:                      "1",
   218  		Rid:                      "hi",
   219  		MediaSourceID:            "SA5",
   220  		Timestamp:                1688978831527.718,
   221  		Type:                     StatsTypeOutboundRTP,
   222  		ID:                       "OT01A2184088143",
   223  		SSRC:                     2184088143,
   224  		Kind:                     "audio",
   225  		TransportID:              "T01",
   226  		CodecID:                  "COT01_111_minptime=10;useinbandfec=1",
   227  		HeaderBytesSent:          24,
   228  		RetransmittedPacketsSent: 25,
   229  		RetransmittedBytesSent:   26,
   230  		FIRCount:                 1,
   231  		PLICount:                 2,
   232  		NACKCount:                3,
   233  		SLICount:                 4,
   234  		QPSum:                    5,
   235  		PacketsSent:              6,
   236  		PacketsDiscardedOnSend:   7,
   237  		FECPacketsSent:           8,
   238  		BytesSent:                9,
   239  		BytesDiscardedOnSend:     10,
   240  		TrackID:                  "d57dbc4b-484b-4b40-9088-d3150e3a2010",
   241  		SenderID:                 "S01",
   242  		RemoteID:                 "ROA2184088143",
   243  		LastPacketSentTimestamp:  11,
   244  		TargetBitrate:            12,
   245  		TotalEncodedBytesTarget:  27,
   246  		FrameWidth:               28,
   247  		FrameHeight:              29,
   248  		FramesPerSecond:          30,
   249  		FramesSent:               31,
   250  		HugeFramesSent:           32,
   251  		FramesEncoded:            13,
   252  		KeyFramesEncoded:         33,
   253  		TotalEncodeTime:          14,
   254  		TotalPacketSendDelay:     34,
   255  		AverageRTCPInterval:      15,
   256  		QualityLimitationReason:  "cpu",
   257  		QualityLimitationDurations: map[string]float64{
   258  			"none":      16,
   259  			"cpu":       17,
   260  			"bandwidth": 18,
   261  			"other":     19,
   262  		},
   263  		QualityLimitationResolutionChanges: 35,
   264  		PerDSCPPacketsSent: map[string]uint32{
   265  			"123": 23,
   266  		},
   267  		Active: true,
   268  	}
   269  	outboundRTPStreamStatsJSON := `
   270  {
   271    "mid": "1",
   272    "rid": "hi",
   273    "mediaSourceId": "SA5",
   274    "timestamp": 1688978831527.718,
   275    "type": "outbound-rtp",
   276    "id": "OT01A2184088143",
   277    "ssrc": 2184088143,
   278    "kind": "audio",
   279    "transportId": "T01",
   280    "codecId": "COT01_111_minptime=10;useinbandfec=1",
   281    "headerBytesSent": 24,
   282    "retransmittedPacketsSent": 25,
   283    "retransmittedBytesSent": 26,
   284    "firCount": 1,
   285    "pliCount": 2,
   286    "nackCount": 3,
   287    "sliCount": 4,
   288    "qpSum": 5,
   289    "packetsSent": 6,
   290    "packetsDiscardedOnSend": 7,
   291    "fecPacketsSent": 8,
   292    "bytesSent": 9,
   293    "bytesDiscardedOnSend": 10,
   294    "trackId": "d57dbc4b-484b-4b40-9088-d3150e3a2010",
   295    "senderId": "S01",
   296    "remoteId": "ROA2184088143",
   297    "lastPacketSentTimestamp": 11,
   298    "targetBitrate": 12,
   299    "totalEncodedBytesTarget": 27,
   300    "frameWidth": 28,
   301    "frameHeight": 29,
   302    "framesPerSecond": 30,
   303    "framesSent": 31,
   304    "hugeFramesSent": 32,
   305    "framesEncoded": 13,
   306    "keyFramesEncoded": 33,
   307    "totalEncodeTime": 14,
   308    "totalPacketSendDelay": 34,
   309    "averageRtcpInterval": 15,
   310    "qualityLimitationReason": "cpu",
   311    "qualityLimitationDurations": {
   312      "none": 16,
   313      "cpu": 17,
   314      "bandwidth": 18,
   315      "other": 19
   316    },
   317    "qualityLimitationResolutionChanges": 35,
   318    "perDscpPacketsSent": {
   319      "123": 23
   320    },
   321    "active": true
   322  }
   323  `
   324  	remoteInboundRTPStreamStats := RemoteInboundRTPStreamStats{
   325  		Timestamp:                 1688978831527.718,
   326  		Type:                      StatsTypeRemoteInboundRTP,
   327  		ID:                        "RIA2184088143",
   328  		SSRC:                      2184088143,
   329  		Kind:                      "audio",
   330  		TransportID:               "T01",
   331  		CodecID:                   "COT01_111_minptime=10;useinbandfec=1",
   332  		FIRCount:                  1,
   333  		PLICount:                  2,
   334  		NACKCount:                 3,
   335  		SLICount:                  4,
   336  		QPSum:                     5,
   337  		PacketsReceived:           6,
   338  		PacketsLost:               7,
   339  		Jitter:                    8,
   340  		PacketsDiscarded:          9,
   341  		PacketsRepaired:           10,
   342  		BurstPacketsLost:          11,
   343  		BurstPacketsDiscarded:     12,
   344  		BurstLossCount:            13,
   345  		BurstDiscardCount:         14,
   346  		BurstLossRate:             15,
   347  		BurstDiscardRate:          16,
   348  		GapLossRate:               17,
   349  		GapDiscardRate:            18,
   350  		LocalID:                   "RIA2184088143",
   351  		RoundTripTime:             19,
   352  		TotalRoundTripTime:        21,
   353  		FractionLost:              20,
   354  		RoundTripTimeMeasurements: 22,
   355  	}
   356  	remoteInboundRTPStreamStatsJSON := `
   357  {
   358    "timestamp": 1688978831527.718,
   359    "type": "remote-inbound-rtp",
   360    "id": "RIA2184088143",
   361    "ssrc": 2184088143,
   362    "kind": "audio",
   363    "transportId": "T01",
   364    "codecId": "COT01_111_minptime=10;useinbandfec=1",
   365    "firCount": 1,
   366    "pliCount": 2,
   367    "nackCount": 3,
   368    "sliCount": 4,
   369    "qpSum": 5,
   370    "packetsReceived": 6,
   371    "packetsLost": 7,
   372    "jitter": 8,
   373    "packetsDiscarded": 9,
   374    "packetsRepaired": 10,
   375    "burstPacketsLost": 11,
   376    "burstPacketsDiscarded": 12,
   377    "burstLossCount": 13,
   378    "burstDiscardCount": 14,
   379    "burstLossRate": 15,
   380    "burstDiscardRate": 16,
   381    "gapLossRate": 17,
   382    "gapDiscardRate": 18,
   383    "localId": "RIA2184088143",
   384    "roundTripTime": 19,
   385    "totalRoundTripTime": 21,
   386    "fractionLost": 20,
   387    "roundTripTimeMeasurements": 22
   388  }
   389  `
   390  	remoteOutboundRTPStreamStats := RemoteOutboundRTPStreamStats{
   391  		Timestamp:                 1688978831527.718,
   392  		Type:                      StatsTypeRemoteOutboundRTP,
   393  		ID:                        "ROA2184088143",
   394  		SSRC:                      2184088143,
   395  		Kind:                      "audio",
   396  		TransportID:               "T01",
   397  		CodecID:                   "CIT01_111_minptime=10;useinbandfec=1",
   398  		FIRCount:                  1,
   399  		PLICount:                  2,
   400  		NACKCount:                 3,
   401  		SLICount:                  4,
   402  		QPSum:                     5,
   403  		PacketsSent:               1259,
   404  		PacketsDiscardedOnSend:    6,
   405  		FECPacketsSent:            7,
   406  		BytesSent:                 92654,
   407  		BytesDiscardedOnSend:      8,
   408  		LocalID:                   "IT01A2184088143",
   409  		RemoteTimestamp:           1689668361298,
   410  		ReportsSent:               9,
   411  		RoundTripTime:             10,
   412  		TotalRoundTripTime:        11,
   413  		RoundTripTimeMeasurements: 12,
   414  	}
   415  	remoteOutboundRTPStreamStatsJSON := `
   416  {
   417    "timestamp": 1688978831527.718,
   418    "type": "remote-outbound-rtp",
   419    "id": "ROA2184088143",
   420    "ssrc": 2184088143,
   421    "kind": "audio",
   422    "transportId": "T01",
   423    "codecId": "CIT01_111_minptime=10;useinbandfec=1",
   424    "firCount": 1,
   425    "pliCount": 2,
   426    "nackCount": 3,
   427    "sliCount": 4,
   428    "qpSum": 5,
   429    "packetsSent": 1259,
   430    "packetsDiscardedOnSend": 6,
   431    "fecPacketsSent": 7,
   432    "bytesSent": 92654,
   433    "bytesDiscardedOnSend": 8,
   434    "localId": "IT01A2184088143",
   435    "remoteTimestamp": 1689668361298,
   436    "reportsSent": 9,
   437    "roundTripTime": 10,
   438    "totalRoundTripTime": 11,
   439    "roundTripTimeMeasurements": 12
   440  }
   441  `
   442  	csrcStats := RTPContributingSourceStats{
   443  		Timestamp:            1688978831527.718,
   444  		Type:                 StatsTypeCSRC,
   445  		ID:                   "ROA2184088143",
   446  		ContributorSSRC:      2184088143,
   447  		InboundRTPStreamID:   "IT01A2184088143",
   448  		PacketsContributedTo: 5,
   449  		AudioLevel:           0.3,
   450  	}
   451  	csrcStatsJSON := `
   452  {
   453    "timestamp": 1688978831527.718,
   454    "type": "csrc",
   455    "id": "ROA2184088143",
   456    "contributorSsrc": 2184088143,
   457    "inboundRtpStreamId": "IT01A2184088143",
   458    "packetsContributedTo": 5,
   459    "audioLevel": 0.3
   460  }
   461  `
   462  	audioSourceStats := AudioSourceStats{
   463  		Timestamp:                 1689668364374.479,
   464  		Type:                      StatsTypeMediaSource,
   465  		ID:                        "SA5",
   466  		TrackIdentifier:           "d57dbc4b-484b-4b40-9088-d3150e3a2010",
   467  		Kind:                      "audio",
   468  		AudioLevel:                0.0030518509475997192,
   469  		TotalAudioEnergy:          0.0024927631236904358,
   470  		TotalSamplesDuration:      28.360000000001634,
   471  		EchoReturnLoss:            -30,
   472  		EchoReturnLossEnhancement: 0.17551203072071075,
   473  		DroppedSamplesDuration:    0.1,
   474  		DroppedSamplesEvents:      2,
   475  		TotalCaptureDelay:         0.3,
   476  		TotalSamplesCaptured:      4,
   477  	}
   478  	audioSourceStatsJSON := `
   479  {
   480    "timestamp": 1689668364374.479,
   481    "type": "media-source",
   482    "id": "SA5",
   483    "trackIdentifier": "d57dbc4b-484b-4b40-9088-d3150e3a2010",
   484    "kind": "audio",
   485    "audioLevel": 0.0030518509475997192,
   486    "totalAudioEnergy": 0.0024927631236904358,
   487    "totalSamplesDuration": 28.360000000001634,
   488    "echoReturnLoss": -30,
   489    "echoReturnLossEnhancement": 0.17551203072071075,
   490    "droppedSamplesDuration": 0.1,
   491    "droppedSamplesEvents": 2,
   492    "totalCaptureDelay": 0.3,
   493    "totalSamplesCaptured": 4
   494  }
   495  `
   496  	videoSourceStats := VideoSourceStats{
   497  		Timestamp:       1689668364374.479,
   498  		Type:            StatsTypeMediaSource,
   499  		ID:              "SV6",
   500  		TrackIdentifier: "d7f11739-d395-42e9-af87-5dfa1cc10ee0",
   501  		Kind:            "video",
   502  		Width:           640,
   503  		Height:          480,
   504  		Frames:          850,
   505  		FramesPerSecond: 30,
   506  	}
   507  	videoSourceStatsJSON := `
   508  {
   509    "timestamp": 1689668364374.479,
   510    "type": "media-source",
   511    "id": "SV6",
   512    "trackIdentifier": "d7f11739-d395-42e9-af87-5dfa1cc10ee0",
   513    "kind": "video",
   514    "width": 640,
   515    "height": 480,
   516    "frames": 850,
   517    "framesPerSecond": 30
   518  }
   519  `
   520  	audioPlayoutStats := AudioPlayoutStats{
   521  		Timestamp:                  1689668364374.181,
   522  		Type:                       StatsTypeMediaPlayout,
   523  		ID:                         "AP",
   524  		Kind:                       "audio",
   525  		SynthesizedSamplesDuration: 1,
   526  		SynthesizedSamplesEvents:   2,
   527  		TotalSamplesDuration:       593.5,
   528  		TotalPlayoutDelay:          1062194.11536,
   529  		TotalSamplesCount:          28488000,
   530  	}
   531  	audioPlayoutStatsJSON := `
   532  {
   533    "timestamp": 1689668364374.181,
   534    "type": "media-playout",
   535    "id": "AP",
   536    "kind": "audio",
   537    "synthesizedSamplesDuration": 1,
   538    "synthesizedSamplesEvents": 2,
   539    "totalSamplesDuration": 593.5,
   540    "totalPlayoutDelay": 1062194.11536,
   541    "totalSamplesCount": 28488000
   542  }
   543  `
   544  	peerConnectionStats := PeerConnectionStats{
   545  		Timestamp:             1688978831527.718,
   546  		Type:                  StatsTypePeerConnection,
   547  		ID:                    "P",
   548  		DataChannelsOpened:    1,
   549  		DataChannelsClosed:    2,
   550  		DataChannelsRequested: 3,
   551  		DataChannelsAccepted:  4,
   552  	}
   553  	peerConnectionStatsJSON := `
   554  {
   555    "timestamp": 1688978831527.718,
   556    "type": "peer-connection",
   557    "id": "P",
   558    "dataChannelsOpened": 1,
   559    "dataChannelsClosed": 2,
   560    "dataChannelsRequested": 3,
   561    "dataChannelsAccepted": 4
   562  }
   563  `
   564  	dataChannelStats := DataChannelStats{
   565  		Timestamp:             1688978831527.718,
   566  		Type:                  StatsTypeDataChannel,
   567  		ID:                    "D1",
   568  		Label:                 "display",
   569  		Protocol:              "protocol",
   570  		DataChannelIdentifier: 1,
   571  		TransportID:           "T1",
   572  		State:                 DataChannelStateOpen,
   573  		MessagesSent:          1,
   574  		BytesSent:             16,
   575  		MessagesReceived:      2,
   576  		BytesReceived:         20,
   577  	}
   578  	dataChannelStatsJSON := `
   579  {
   580    "timestamp": 1688978831527.718,
   581    "type": "data-channel",
   582    "id": "D1",
   583    "label": "display",
   584    "protocol": "protocol",
   585    "dataChannelIdentifier": 1,
   586    "transportId": "T1",
   587    "state": "open",
   588    "messagesSent": 1,
   589    "bytesSent": 16,
   590    "messagesReceived": 2,
   591    "bytesReceived": 20
   592  }
   593  `
   594  	streamStats := MediaStreamStats{
   595  		Timestamp:        1688978831527.718,
   596  		Type:             StatsTypeStream,
   597  		ID:               "ROA2184088143",
   598  		StreamIdentifier: "S1",
   599  		TrackIDs:         []string{"d57dbc4b-484b-4b40-9088-d3150e3a2010"},
   600  	}
   601  	streamStatsJSON := `
   602  {
   603    "timestamp": 1688978831527.718,
   604    "type": "stream",
   605    "id": "ROA2184088143",
   606    "streamIdentifier": "S1",
   607    "trackIds": [
   608      "d57dbc4b-484b-4b40-9088-d3150e3a2010"
   609    ]
   610  }
   611  `
   612  	senderVideoTrackAttachmentStats := SenderVideoTrackAttachmentStats{
   613  		Timestamp:      1688978831527.718,
   614  		Type:           StatsTypeTrack,
   615  		ID:             "S2",
   616  		Kind:           "video",
   617  		FramesCaptured: 1,
   618  		FramesSent:     2,
   619  		HugeFramesSent: 3,
   620  		KeyFramesSent:  4,
   621  	}
   622  	senderVideoTrackAttachmentStatsJSON := `
   623  {
   624    "timestamp": 1688978831527.718,
   625    "type": "track",
   626    "id": "S2",
   627    "kind": "video",
   628    "framesCaptured": 1,
   629    "framesSent": 2,
   630    "hugeFramesSent": 3,
   631    "keyFramesSent": 4
   632  }
   633  `
   634  	senderAudioTrackAttachmentStats := SenderAudioTrackAttachmentStats{
   635  		Timestamp:                 1688978831527.718,
   636  		Type:                      StatsTypeTrack,
   637  		ID:                        "S1",
   638  		TrackIdentifier:           "audio",
   639  		RemoteSource:              true,
   640  		Ended:                     true,
   641  		Kind:                      "audio",
   642  		AudioLevel:                0.1,
   643  		TotalAudioEnergy:          0.2,
   644  		VoiceActivityFlag:         true,
   645  		TotalSamplesDuration:      0.3,
   646  		EchoReturnLoss:            0.4,
   647  		EchoReturnLossEnhancement: 0.5,
   648  		TotalSamplesSent:          200,
   649  	}
   650  	senderAudioTrackAttachmentStatsJSON := `
   651  {
   652    "timestamp": 1688978831527.718,
   653    "type": "track",
   654    "id": "S1",
   655    "trackIdentifier": "audio",
   656    "remoteSource": true,
   657    "ended": true,
   658    "kind": "audio",
   659    "audioLevel": 0.1,
   660    "totalAudioEnergy": 0.2,
   661    "voiceActivityFlag": true,
   662    "totalSamplesDuration": 0.3,
   663    "echoReturnLoss": 0.4,
   664    "echoReturnLossEnhancement": 0.5,
   665    "totalSamplesSent": 200
   666  }
   667  `
   668  	videoSenderStats := VideoSenderStats{
   669  		Timestamp:      1688978831527.718,
   670  		Type:           StatsTypeSender,
   671  		ID:             "S2",
   672  		Kind:           "video",
   673  		FramesCaptured: 1,
   674  		FramesSent:     2,
   675  		HugeFramesSent: 3,
   676  		KeyFramesSent:  4,
   677  	}
   678  	videoSenderStatsJSON := `
   679  {
   680    "timestamp": 1688978831527.718,
   681    "type": "sender",
   682    "id": "S2",
   683    "kind": "video",
   684    "framesCaptured": 1,
   685    "framesSent": 2,
   686    "hugeFramesSent": 3,
   687    "keyFramesSent": 4
   688  }
   689  `
   690  	audioSenderStats := AudioSenderStats{
   691  		Timestamp:                 1688978831527.718,
   692  		Type:                      StatsTypeSender,
   693  		ID:                        "S1",
   694  		TrackIdentifier:           "audio",
   695  		RemoteSource:              true,
   696  		Ended:                     true,
   697  		Kind:                      "audio",
   698  		AudioLevel:                0.1,
   699  		TotalAudioEnergy:          0.2,
   700  		VoiceActivityFlag:         true,
   701  		TotalSamplesDuration:      0.3,
   702  		EchoReturnLoss:            0.4,
   703  		EchoReturnLossEnhancement: 0.5,
   704  		TotalSamplesSent:          200,
   705  	}
   706  	audioSenderStatsJSON := `
   707  {
   708    "timestamp": 1688978831527.718,
   709    "type": "sender",
   710    "id": "S1",
   711    "trackIdentifier": "audio",
   712    "remoteSource": true,
   713    "ended": true,
   714    "kind": "audio",
   715    "audioLevel": 0.1,
   716    "totalAudioEnergy": 0.2,
   717    "voiceActivityFlag": true,
   718    "totalSamplesDuration": 0.3,
   719    "echoReturnLoss": 0.4,
   720    "echoReturnLossEnhancement": 0.5,
   721    "totalSamplesSent": 200
   722  }
   723  `
   724  	videoReceiverStats := VideoReceiverStats{
   725  		Timestamp:                 1688978831527.718,
   726  		Type:                      StatsTypeReceiver,
   727  		ID:                        "ROA2184088143",
   728  		Kind:                      "video",
   729  		FrameWidth:                720,
   730  		FrameHeight:               480,
   731  		FramesPerSecond:           30.0,
   732  		EstimatedPlayoutTimestamp: 1688978831527.718,
   733  		JitterBufferDelay:         0.1,
   734  		JitterBufferEmittedCount:  1,
   735  		FramesReceived:            79,
   736  		KeyFramesReceived:         10,
   737  		FramesDecoded:             10,
   738  		FramesDropped:             10,
   739  		PartialFramesLost:         5,
   740  		FullFramesLost:            5,
   741  	}
   742  	videoReceiverStatsJSON := `
   743  {
   744    "timestamp": 1688978831527.718,
   745    "type": "receiver",
   746    "id": "ROA2184088143",
   747    "kind": "video",
   748    "frameWidth": 720,
   749    "frameHeight": 480,
   750    "framesPerSecond": 30.0,
   751    "estimatedPlayoutTimestamp": 1688978831527.718,
   752    "jitterBufferDelay": 0.1,
   753    "jitterBufferEmittedCount": 1,
   754    "framesReceived": 79,
   755    "keyFramesReceived": 10,
   756    "framesDecoded": 10,
   757    "framesDropped": 10,
   758    "partialFramesLost": 5,
   759    "fullFramesLost": 5
   760  }
   761  `
   762  	audioReceiverStats := AudioReceiverStats{
   763  		Timestamp:                 1688978831527.718,
   764  		Type:                      StatsTypeReceiver,
   765  		ID:                        "R1",
   766  		Kind:                      "audio",
   767  		AudioLevel:                0.1,
   768  		TotalAudioEnergy:          0.2,
   769  		VoiceActivityFlag:         true,
   770  		TotalSamplesDuration:      0.3,
   771  		EstimatedPlayoutTimestamp: 1688978831527.718,
   772  		JitterBufferDelay:         0.5,
   773  		JitterBufferEmittedCount:  6,
   774  		TotalSamplesReceived:      7,
   775  		ConcealedSamples:          8,
   776  		ConcealmentEvents:         9,
   777  	}
   778  	audioReceiverStatsJSON := `
   779  {
   780    "timestamp": 1688978831527.718,
   781    "type": "receiver",
   782    "id": "R1",
   783    "kind": "audio",
   784    "audioLevel": 0.1,
   785    "totalAudioEnergy": 0.2,
   786    "voiceActivityFlag": true,
   787    "totalSamplesDuration": 0.3,
   788    "estimatedPlayoutTimestamp": 1688978831527.718,
   789    "jitterBufferDelay": 0.5,
   790    "jitterBufferEmittedCount": 6,
   791    "totalSamplesReceived": 7,
   792    "concealedSamples": 8,
   793    "concealmentEvents": 9
   794  }
   795  `
   796  	transportStats := TransportStats{
   797  		Timestamp:               1688978831527.718,
   798  		Type:                    StatsTypeTransport,
   799  		ID:                      "T01",
   800  		PacketsSent:             60,
   801  		PacketsReceived:         8,
   802  		BytesSent:               6517,
   803  		BytesReceived:           1159,
   804  		RTCPTransportStatsID:    "T01",
   805  		ICERole:                 ICERoleControlling,
   806  		DTLSState:               DTLSTransportStateConnected,
   807  		ICEState:                ICETransportStateConnected,
   808  		SelectedCandidatePairID: "CPxIhBDNnT_sPDhy1TB",
   809  		LocalCertificateID:      "CFF4:4F:C4:C7:F3:31:6C:B9:D5:AD:19:64:05:9F:2F:E9:00:70:56:1E:BA:92:29:3A:08:CE:1B:27:CF:2D:AB:24",
   810  		RemoteCertificateID:     "CF62:AF:88:F7:F3:0F:D6:C4:93:91:1E:AD:52:F0:A4:12:04:F9:48:E7:06:16:BA:A3:86:26:8F:1E:38:1C:48:49",
   811  		DTLSCipher:              "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
   812  		SRTPCipher:              "AES_CM_128_HMAC_SHA1_80",
   813  	}
   814  	transportStatsJSON := `
   815  {
   816    "timestamp": 1688978831527.718,
   817    "type": "transport",
   818    "id": "T01",
   819    "packetsSent": 60,
   820    "packetsReceived": 8,
   821    "bytesSent": 6517,
   822    "bytesReceived": 1159,
   823    "rtcpTransportStatsId": "T01",
   824    "iceRole": "controlling",
   825    "dtlsState": "connected",
   826    "iceState": "connected",
   827    "selectedCandidatePairId": "CPxIhBDNnT_sPDhy1TB",
   828    "localCertificateId": "CFF4:4F:C4:C7:F3:31:6C:B9:D5:AD:19:64:05:9F:2F:E9:00:70:56:1E:BA:92:29:3A:08:CE:1B:27:CF:2D:AB:24",
   829    "remoteCertificateId": "CF62:AF:88:F7:F3:0F:D6:C4:93:91:1E:AD:52:F0:A4:12:04:F9:48:E7:06:16:BA:A3:86:26:8F:1E:38:1C:48:49",
   830    "dtlsCipher": "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
   831    "srtpCipher": "AES_CM_128_HMAC_SHA1_80"
   832  }
   833  `
   834  	iceCandidatePairStats := ICECandidatePairStats{
   835  		Timestamp:                   1688978831527.718,
   836  		Type:                        StatsTypeCandidatePair,
   837  		ID:                          "CPxIhBDNnT_LlMJOnBv",
   838  		TransportID:                 "T01",
   839  		LocalCandidateID:            "IxIhBDNnT",
   840  		RemoteCandidateID:           "ILlMJOnBv",
   841  		State:                       "waiting",
   842  		Nominated:                   true,
   843  		PacketsSent:                 1,
   844  		PacketsReceived:             2,
   845  		BytesSent:                   3,
   846  		BytesReceived:               4,
   847  		LastPacketSentTimestamp:     5,
   848  		LastPacketReceivedTimestamp: 6,
   849  		FirstRequestTimestamp:       7,
   850  		LastRequestTimestamp:        8,
   851  		LastResponseTimestamp:       9,
   852  		TotalRoundTripTime:          10,
   853  		CurrentRoundTripTime:        11,
   854  		AvailableOutgoingBitrate:    12,
   855  		AvailableIncomingBitrate:    13,
   856  		CircuitBreakerTriggerCount:  14,
   857  		RequestsReceived:            15,
   858  		RequestsSent:                16,
   859  		ResponsesReceived:           17,
   860  		ResponsesSent:               18,
   861  		RetransmissionsReceived:     19,
   862  		RetransmissionsSent:         20,
   863  		ConsentRequestsSent:         21,
   864  		ConsentExpiredTimestamp:     22,
   865  		PacketsDiscardedOnSend:      23,
   866  		BytesDiscardedOnSend:        24,
   867  	}
   868  	iceCandidatePairStatsJSON := `
   869  {
   870    "timestamp": 1688978831527.718,
   871    "type": "candidate-pair",
   872    "id": "CPxIhBDNnT_LlMJOnBv",
   873    "transportId": "T01",
   874    "localCandidateId": "IxIhBDNnT",
   875    "remoteCandidateId": "ILlMJOnBv",
   876    "state": "waiting",
   877    "nominated": true,
   878    "packetsSent": 1,
   879    "packetsReceived": 2,
   880    "bytesSent": 3,
   881    "bytesReceived": 4,
   882    "lastPacketSentTimestamp": 5,
   883    "lastPacketReceivedTimestamp": 6,
   884    "firstRequestTimestamp": 7,
   885    "lastRequestTimestamp": 8,
   886    "lastResponseTimestamp": 9,
   887    "totalRoundTripTime": 10,
   888    "currentRoundTripTime": 11,
   889    "availableOutgoingBitrate": 12,
   890    "availableIncomingBitrate": 13,
   891    "circuitBreakerTriggerCount": 14,
   892    "requestsReceived": 15,
   893    "requestsSent": 16,
   894    "responsesReceived": 17,
   895    "responsesSent": 18,
   896    "retransmissionsReceived": 19,
   897    "retransmissionsSent": 20,
   898    "consentRequestsSent": 21,
   899    "consentExpiredTimestamp": 22,
   900    "packetsDiscardedOnSend": 23,
   901    "bytesDiscardedOnSend": 24
   902  }
   903  `
   904  	localIceCandidateStats := ICECandidateStats{
   905  		Timestamp:     1688978831527.718,
   906  		Type:          StatsTypeLocalCandidate,
   907  		ID:            "ILO8S8KYr",
   908  		TransportID:   "T01",
   909  		NetworkType:   "wifi",
   910  		IP:            "192.168.0.36",
   911  		Port:          65400,
   912  		Protocol:      "udp",
   913  		CandidateType: ICECandidateTypeHost,
   914  		Priority:      2122260223,
   915  		URL:           "example.com",
   916  		RelayProtocol: "tcp",
   917  		Deleted:       true,
   918  	}
   919  	localIceCandidateStatsJSON := `
   920  {
   921    "timestamp": 1688978831527.718,
   922    "type": "local-candidate",
   923    "id": "ILO8S8KYr",
   924    "transportId": "T01",
   925    "networkType": "wifi",
   926    "ip": "192.168.0.36",
   927    "port": 65400,
   928    "protocol": "udp",
   929    "candidateType": "host",
   930    "priority": 2122260223,
   931    "url": "example.com",
   932    "relayProtocol": "tcp",
   933    "deleted": true
   934  }
   935  `
   936  	remoteIceCandidateStats := ICECandidateStats{
   937  		Timestamp:     1689668364374.181,
   938  		Type:          StatsTypeRemoteCandidate,
   939  		ID:            "IGPGeswsH",
   940  		TransportID:   "T01",
   941  		IP:            "10.213.237.226",
   942  		Port:          50618,
   943  		Protocol:      "udp",
   944  		CandidateType: ICECandidateTypeHost,
   945  		Priority:      2122194687,
   946  		URL:           "example.com",
   947  		RelayProtocol: "tcp",
   948  		Deleted:       true,
   949  	}
   950  	remoteIceCandidateStatsJSON := `
   951  {
   952    "timestamp": 1689668364374.181,
   953    "type": "remote-candidate",
   954    "id": "IGPGeswsH",
   955    "transportId": "T01",
   956    "ip": "10.213.237.226",
   957    "port": 50618,
   958    "protocol": "udp",
   959    "candidateType": "host",
   960    "priority": 2122194687,
   961    "url": "example.com",
   962    "relayProtocol": "tcp",
   963    "deleted": true
   964  }
   965  `
   966  	certificateStats := CertificateStats{
   967  		Timestamp:            1689668364374.479,
   968  		Type:                 StatsTypeCertificate,
   969  		ID:                   "CF23:AB:FA:0B:0E:DF:12:34:D3:6C:EA:83:43:BD:79:39:87:39:11:49:41:8A:63:0E:17:B1:3F:94:FA:E3:62:20",
   970  		Fingerprint:          "23:AB:FA:0B:0E:DF:12:34:D3:6C:EA:83:43:BD:79:39:87:39:11:49:41:8A:63:0E:17:B1:3F:94:FA:E3:62:20",
   971  		FingerprintAlgorithm: "sha-256",
   972  		Base64Certificate:    "MIIBFjCBvKADAgECAggAwlrxojpmgTAKBggqhkjOPQQDAjARMQ8wDQYDVQQDDAZXZWJSVEMwHhcNMjMwNzE3MDgxODU2WhcNMjMwODE3MDgxODU2WjARMQ8wDQYDVQQDDAZXZWJSVEMwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARKETeS9qNGe3ltwp+q2KgsYWsJLFCJGap4L2aa862sPijHeuzLgO2bju/mosJN0Li7mXhuKBOsCkCMU7vZHVVVMAoGCCqGSM49BAMCA0kAMEYCIQDXyuyMMrgzd+w3c4h3vPn9AzLcf9CHVHRGYyy5ReI/hgIhALkXfaZ96TQRf5FI2mBJJUX9O/q4Poe3wNZxxWeDcYN+",
   973  		IssuerCertificateID:  "CF62:AF:88:F7:F3:0F:D6:C4:93:91:1E:AD:52:F0:A4:12:04:F9:48:E7:06:16:BA:A3:86:26:8F:1E:38:1C:48:49",
   974  	}
   975  	certificateStatsJSON := `
   976  {
   977    "timestamp": 1689668364374.479,
   978    "type": "certificate",
   979    "id": "CF23:AB:FA:0B:0E:DF:12:34:D3:6C:EA:83:43:BD:79:39:87:39:11:49:41:8A:63:0E:17:B1:3F:94:FA:E3:62:20",
   980    "fingerprint": "23:AB:FA:0B:0E:DF:12:34:D3:6C:EA:83:43:BD:79:39:87:39:11:49:41:8A:63:0E:17:B1:3F:94:FA:E3:62:20",
   981    "fingerprintAlgorithm": "sha-256",
   982    "base64Certificate": "MIIBFjCBvKADAgECAggAwlrxojpmgTAKBggqhkjOPQQDAjARMQ8wDQYDVQQDDAZXZWJSVEMwHhcNMjMwNzE3MDgxODU2WhcNMjMwODE3MDgxODU2WjARMQ8wDQYDVQQDDAZXZWJSVEMwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARKETeS9qNGe3ltwp+q2KgsYWsJLFCJGap4L2aa862sPijHeuzLgO2bju/mosJN0Li7mXhuKBOsCkCMU7vZHVVVMAoGCCqGSM49BAMCA0kAMEYCIQDXyuyMMrgzd+w3c4h3vPn9AzLcf9CHVHRGYyy5ReI/hgIhALkXfaZ96TQRf5FI2mBJJUX9O/q4Poe3wNZxxWeDcYN+",
   983    "issuerCertificateId": "CF62:AF:88:F7:F3:0F:D6:C4:93:91:1E:AD:52:F0:A4:12:04:F9:48:E7:06:16:BA:A3:86:26:8F:1E:38:1C:48:49"
   984  }
   985  `
   986  
   987  	return []statSample{
   988  		{
   989  			name:  "codec_stats",
   990  			stats: codecStats,
   991  			json:  codecStatsJSON,
   992  		},
   993  		{
   994  			name:  "inbound_rtp_stream_stats",
   995  			stats: inboundRTPStreamStats,
   996  			json:  inboundRTPStreamStatsJSON,
   997  		},
   998  		{
   999  			name:  "outbound_rtp_stream_stats",
  1000  			stats: outboundRTPStreamStats,
  1001  			json:  outboundRTPStreamStatsJSON,
  1002  		},
  1003  		{
  1004  			name:  "remote_inbound_rtp_stream_stats",
  1005  			stats: remoteInboundRTPStreamStats,
  1006  			json:  remoteInboundRTPStreamStatsJSON,
  1007  		},
  1008  		{
  1009  			name:  "remote_outbound_rtp_stream_stats",
  1010  			stats: remoteOutboundRTPStreamStats,
  1011  			json:  remoteOutboundRTPStreamStatsJSON,
  1012  		},
  1013  		{
  1014  			name:  "rtp_contributing_source_stats",
  1015  			stats: csrcStats,
  1016  			json:  csrcStatsJSON,
  1017  		},
  1018  		{
  1019  			name:  "audio_source_stats",
  1020  			stats: audioSourceStats,
  1021  			json:  audioSourceStatsJSON,
  1022  		},
  1023  		{
  1024  			name:  "video_source_stats",
  1025  			stats: videoSourceStats,
  1026  			json:  videoSourceStatsJSON,
  1027  		},
  1028  		{
  1029  			name:  "audio_playout_stats",
  1030  			stats: audioPlayoutStats,
  1031  			json:  audioPlayoutStatsJSON,
  1032  		},
  1033  		{
  1034  			name:  "peer_connection_stats",
  1035  			stats: peerConnectionStats,
  1036  			json:  peerConnectionStatsJSON,
  1037  		},
  1038  		{
  1039  			name:  "data_channel_stats",
  1040  			stats: dataChannelStats,
  1041  			json:  dataChannelStatsJSON,
  1042  		},
  1043  		{
  1044  			name:  "media_stream_stats",
  1045  			stats: streamStats,
  1046  			json:  streamStatsJSON,
  1047  		},
  1048  		{
  1049  			name:  "sender_video_track_stats",
  1050  			stats: senderVideoTrackAttachmentStats,
  1051  			json:  senderVideoTrackAttachmentStatsJSON,
  1052  		},
  1053  		{
  1054  			name:  "sender_audio_track_stats",
  1055  			stats: senderAudioTrackAttachmentStats,
  1056  			json:  senderAudioTrackAttachmentStatsJSON,
  1057  		},
  1058  		{
  1059  			name:  "receiver_video_track_stats",
  1060  			stats: videoSenderStats,
  1061  			json:  videoSenderStatsJSON,
  1062  		},
  1063  		{
  1064  			name:  "receiver_audio_track_stats",
  1065  			stats: audioSenderStats,
  1066  			json:  audioSenderStatsJSON,
  1067  		},
  1068  		{
  1069  			name:  "receiver_video_track_stats",
  1070  			stats: videoReceiverStats,
  1071  			json:  videoReceiverStatsJSON,
  1072  		},
  1073  		{
  1074  			name:  "receiver_audio_track_stats",
  1075  			stats: audioReceiverStats,
  1076  			json:  audioReceiverStatsJSON,
  1077  		},
  1078  		{
  1079  			name:  "transport_stats",
  1080  			stats: transportStats,
  1081  			json:  transportStatsJSON,
  1082  		},
  1083  		{
  1084  			name:  "ice_candidate_pair_stats",
  1085  			stats: iceCandidatePairStats,
  1086  			json:  iceCandidatePairStatsJSON,
  1087  		},
  1088  		{
  1089  			name:  "local_ice_candidate_stats",
  1090  			stats: localIceCandidateStats,
  1091  			json:  localIceCandidateStatsJSON,
  1092  		},
  1093  		{
  1094  			name:  "remote_ice_candidate_stats",
  1095  			stats: remoteIceCandidateStats,
  1096  			json:  remoteIceCandidateStatsJSON,
  1097  		},
  1098  		{
  1099  			name:  "certificate_stats",
  1100  			stats: certificateStats,
  1101  			json:  certificateStatsJSON,
  1102  		},
  1103  	}
  1104  }
  1105  
  1106  func TestStatsMarshal(t *testing.T) {
  1107  	for _, test := range getStatsSamples() {
  1108  		t.Run(test.name+"_marshal", func(t *testing.T) {
  1109  			actualJSON, err := json.Marshal(test.stats)
  1110  			require.NoError(t, err)
  1111  
  1112  			assert.JSONEq(t, test.json, string(actualJSON))
  1113  		})
  1114  	}
  1115  }
  1116  
  1117  func TestStatsUnmarshal(t *testing.T) {
  1118  	for _, test := range getStatsSamples() {
  1119  		t.Run(test.name+"_unmarshal", func(t *testing.T) {
  1120  			actualStats, err := UnmarshalStatsJSON([]byte(test.json))
  1121  			require.NoError(t, err)
  1122  
  1123  			assert.Equal(t, test.stats, actualStats)
  1124  		})
  1125  	}
  1126  }
  1127  
  1128  func waitWithTimeout(t *testing.T, wg *sync.WaitGroup) {
  1129  	// Wait for all of the event handlers to be triggered.
  1130  	done := make(chan struct{})
  1131  	go func() {
  1132  		wg.Wait()
  1133  		done <- struct{}{}
  1134  	}()
  1135  	timeout := time.After(5 * time.Second)
  1136  	select {
  1137  	case <-done:
  1138  		break
  1139  	case <-timeout:
  1140  		t.Fatal("timed out waiting for waitgroup")
  1141  	}
  1142  }
  1143  
  1144  func getConnectionStats(t *testing.T, report StatsReport, pc *PeerConnection) PeerConnectionStats {
  1145  	stats, ok := report.GetConnectionStats(pc)
  1146  	assert.True(t, ok)
  1147  	assert.Equal(t, stats.Type, StatsTypePeerConnection)
  1148  	return stats
  1149  }
  1150  
  1151  func getDataChannelStats(t *testing.T, report StatsReport, dc *DataChannel) DataChannelStats {
  1152  	stats, ok := report.GetDataChannelStats(dc)
  1153  	assert.True(t, ok)
  1154  	assert.Equal(t, stats.Type, StatsTypeDataChannel)
  1155  	return stats
  1156  }
  1157  
  1158  func getCodecStats(t *testing.T, report StatsReport, c *RTPCodecParameters) CodecStats {
  1159  	stats, ok := report.GetCodecStats(c)
  1160  	assert.True(t, ok)
  1161  	assert.Equal(t, stats.Type, StatsTypeCodec)
  1162  	return stats
  1163  }
  1164  
  1165  func getTransportStats(t *testing.T, report StatsReport, statsID string) TransportStats {
  1166  	stats, ok := report[statsID]
  1167  	assert.True(t, ok)
  1168  	transportStats, ok := stats.(TransportStats)
  1169  	assert.True(t, ok)
  1170  	assert.Equal(t, transportStats.Type, StatsTypeTransport)
  1171  	return transportStats
  1172  }
  1173  
  1174  func getSctpTransportStats(t *testing.T, report StatsReport) SCTPTransportStats {
  1175  	stats, ok := report["sctpTransport"]
  1176  	assert.True(t, ok)
  1177  	transportStats, ok := stats.(SCTPTransportStats)
  1178  	assert.True(t, ok)
  1179  	assert.Equal(t, transportStats.Type, StatsTypeSCTPTransport)
  1180  	return transportStats
  1181  }
  1182  
  1183  func getCertificateStats(t *testing.T, report StatsReport, certificate *Certificate) CertificateStats {
  1184  	certificateStats, ok := report.GetCertificateStats(certificate)
  1185  	assert.True(t, ok)
  1186  	assert.Equal(t, certificateStats.Type, StatsTypeCertificate)
  1187  	return certificateStats
  1188  }
  1189  
  1190  func findLocalCandidateStats(report StatsReport) []ICECandidateStats {
  1191  	result := []ICECandidateStats{}
  1192  	for _, s := range report {
  1193  		stats, ok := s.(ICECandidateStats)
  1194  		if ok && stats.Type == StatsTypeLocalCandidate {
  1195  			result = append(result, stats)
  1196  		}
  1197  	}
  1198  	return result
  1199  }
  1200  
  1201  func findRemoteCandidateStats(report StatsReport) []ICECandidateStats {
  1202  	result := []ICECandidateStats{}
  1203  	for _, s := range report {
  1204  		stats, ok := s.(ICECandidateStats)
  1205  		if ok && stats.Type == StatsTypeRemoteCandidate {
  1206  			result = append(result, stats)
  1207  		}
  1208  	}
  1209  	return result
  1210  }
  1211  
  1212  func findCandidatePairStats(t *testing.T, report StatsReport) []ICECandidatePairStats {
  1213  	result := []ICECandidatePairStats{}
  1214  	for _, s := range report {
  1215  		stats, ok := s.(ICECandidatePairStats)
  1216  		if ok {
  1217  			assert.Equal(t, StatsTypeCandidatePair, stats.Type)
  1218  			result = append(result, stats)
  1219  		}
  1220  	}
  1221  	return result
  1222  }
  1223  
  1224  func signalPairForStats(pcOffer *PeerConnection, pcAnswer *PeerConnection) error {
  1225  	offerChan := make(chan SessionDescription)
  1226  	pcOffer.OnICECandidate(func(candidate *ICECandidate) {
  1227  		if candidate == nil {
  1228  			offerChan <- *pcOffer.PendingLocalDescription()
  1229  		}
  1230  	})
  1231  
  1232  	offer, err := pcOffer.CreateOffer(nil)
  1233  	if err != nil {
  1234  		return err
  1235  	}
  1236  	if err := pcOffer.SetLocalDescription(offer); err != nil {
  1237  		return err
  1238  	}
  1239  
  1240  	timeout := time.After(3 * time.Second)
  1241  	select {
  1242  	case <-timeout:
  1243  		return errReceiveOfferTimeout
  1244  	case offer := <-offerChan:
  1245  		if err := pcAnswer.SetRemoteDescription(offer); err != nil {
  1246  			return err
  1247  		}
  1248  
  1249  		answer, err := pcAnswer.CreateAnswer(nil)
  1250  		if err != nil {
  1251  			return err
  1252  		}
  1253  
  1254  		if err = pcAnswer.SetLocalDescription(answer); err != nil {
  1255  			return err
  1256  		}
  1257  
  1258  		err = pcOffer.SetRemoteDescription(answer)
  1259  		if err != nil {
  1260  			return err
  1261  		}
  1262  		return nil
  1263  	}
  1264  }
  1265  
  1266  func TestStatsConvertState(t *testing.T) {
  1267  	testCases := []struct {
  1268  		ice   ice.CandidatePairState
  1269  		stats StatsICECandidatePairState
  1270  	}{
  1271  		{
  1272  			ice.CandidatePairStateWaiting,
  1273  			StatsICECandidatePairStateWaiting,
  1274  		},
  1275  		{
  1276  			ice.CandidatePairStateInProgress,
  1277  			StatsICECandidatePairStateInProgress,
  1278  		},
  1279  		{
  1280  			ice.CandidatePairStateFailed,
  1281  			StatsICECandidatePairStateFailed,
  1282  		},
  1283  		{
  1284  			ice.CandidatePairStateSucceeded,
  1285  			StatsICECandidatePairStateSucceeded,
  1286  		},
  1287  	}
  1288  
  1289  	s, err := toStatsICECandidatePairState(ice.CandidatePairState(42))
  1290  
  1291  	assert.Error(t, err)
  1292  	assert.Equal(t,
  1293  		StatsICECandidatePairState("Unknown"),
  1294  		s)
  1295  	for i, testCase := range testCases {
  1296  		s, err := toStatsICECandidatePairState(testCase.ice)
  1297  		assert.NoError(t, err)
  1298  		assert.Equal(t,
  1299  			testCase.stats,
  1300  			s,
  1301  			"testCase: %d %v", i, testCase,
  1302  		)
  1303  	}
  1304  }
  1305  
  1306  func TestPeerConnection_GetStats(t *testing.T) {
  1307  	offerPC, answerPC, err := newPair()
  1308  	assert.NoError(t, err)
  1309  
  1310  	track1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion1")
  1311  	require.NoError(t, err)
  1312  
  1313  	_, err = offerPC.AddTrack(track1)
  1314  	require.NoError(t, err)
  1315  
  1316  	baseLineReportPCOffer := offerPC.GetStats()
  1317  	baseLineReportPCAnswer := answerPC.GetStats()
  1318  
  1319  	connStatsOffer := getConnectionStats(t, baseLineReportPCOffer, offerPC)
  1320  	connStatsAnswer := getConnectionStats(t, baseLineReportPCAnswer, answerPC)
  1321  
  1322  	for _, connStats := range []PeerConnectionStats{connStatsOffer, connStatsAnswer} {
  1323  		assert.Equal(t, uint32(0), connStats.DataChannelsOpened)
  1324  		assert.Equal(t, uint32(0), connStats.DataChannelsClosed)
  1325  		assert.Equal(t, uint32(0), connStats.DataChannelsRequested)
  1326  		assert.Equal(t, uint32(0), connStats.DataChannelsAccepted)
  1327  	}
  1328  
  1329  	// Create a DC, open it and send a message
  1330  	offerDC, err := offerPC.CreateDataChannel("offerDC", nil)
  1331  	assert.NoError(t, err)
  1332  
  1333  	msg := []byte("a classic test message")
  1334  	offerDC.OnOpen(func() {
  1335  		assert.NoError(t, offerDC.Send(msg))
  1336  	})
  1337  
  1338  	dcWait := sync.WaitGroup{}
  1339  	dcWait.Add(1)
  1340  
  1341  	answerDCChan := make(chan *DataChannel)
  1342  	answerPC.OnDataChannel(func(d *DataChannel) {
  1343  		d.OnOpen(func() {
  1344  			answerDCChan <- d
  1345  		})
  1346  		d.OnMessage(func(DataChannelMessage) {
  1347  			dcWait.Done()
  1348  		})
  1349  	})
  1350  
  1351  	assert.NoError(t, signalPairForStats(offerPC, answerPC))
  1352  	waitWithTimeout(t, &dcWait)
  1353  
  1354  	answerDC := <-answerDCChan
  1355  
  1356  	reportPCOffer := offerPC.GetStats()
  1357  	reportPCAnswer := answerPC.GetStats()
  1358  
  1359  	connStatsOffer = getConnectionStats(t, reportPCOffer, offerPC)
  1360  	assert.Equal(t, uint32(1), connStatsOffer.DataChannelsOpened)
  1361  	assert.Equal(t, uint32(0), connStatsOffer.DataChannelsClosed)
  1362  	assert.Equal(t, uint32(1), connStatsOffer.DataChannelsRequested)
  1363  	assert.Equal(t, uint32(0), connStatsOffer.DataChannelsAccepted)
  1364  	dcStatsOffer := getDataChannelStats(t, reportPCOffer, offerDC)
  1365  	assert.Equal(t, DataChannelStateOpen, dcStatsOffer.State)
  1366  	assert.Equal(t, uint32(1), dcStatsOffer.MessagesSent)
  1367  	assert.Equal(t, uint64(len(msg)), dcStatsOffer.BytesSent)
  1368  	assert.NotEmpty(t, findLocalCandidateStats(reportPCOffer))
  1369  	assert.NotEmpty(t, findRemoteCandidateStats(reportPCOffer))
  1370  	assert.NotEmpty(t, findCandidatePairStats(t, reportPCOffer))
  1371  
  1372  	connStatsAnswer = getConnectionStats(t, reportPCAnswer, answerPC)
  1373  	assert.Equal(t, uint32(1), connStatsAnswer.DataChannelsOpened)
  1374  	assert.Equal(t, uint32(0), connStatsAnswer.DataChannelsClosed)
  1375  	assert.Equal(t, uint32(0), connStatsAnswer.DataChannelsRequested)
  1376  	assert.Equal(t, uint32(1), connStatsAnswer.DataChannelsAccepted)
  1377  	dcStatsAnswer := getDataChannelStats(t, reportPCAnswer, answerDC)
  1378  	assert.Equal(t, DataChannelStateOpen, dcStatsAnswer.State)
  1379  	assert.Equal(t, uint32(1), dcStatsAnswer.MessagesReceived)
  1380  	assert.Equal(t, uint64(len(msg)), dcStatsAnswer.BytesReceived)
  1381  	assert.NotEmpty(t, findLocalCandidateStats(reportPCAnswer))
  1382  	assert.NotEmpty(t, findRemoteCandidateStats(reportPCAnswer))
  1383  	assert.NotEmpty(t, findCandidatePairStats(t, reportPCAnswer))
  1384  	assert.NoError(t, err)
  1385  	for i := range offerPC.api.mediaEngine.videoCodecs {
  1386  		codecStat := getCodecStats(t, reportPCOffer, &(offerPC.api.mediaEngine.videoCodecs[i]))
  1387  		assert.NotEmpty(t, codecStat)
  1388  	}
  1389  	for i := range offerPC.api.mediaEngine.audioCodecs {
  1390  		codecStat := getCodecStats(t, reportPCOffer, &(offerPC.api.mediaEngine.audioCodecs[i]))
  1391  		assert.NotEmpty(t, codecStat)
  1392  	}
  1393  
  1394  	// Close answer DC now
  1395  	dcWait = sync.WaitGroup{}
  1396  	dcWait.Add(1)
  1397  	offerDC.OnClose(func() {
  1398  		dcWait.Done()
  1399  	})
  1400  	assert.NoError(t, answerDC.Close())
  1401  	waitWithTimeout(t, &dcWait)
  1402  	time.Sleep(10 * time.Millisecond)
  1403  
  1404  	reportPCOffer = offerPC.GetStats()
  1405  	reportPCAnswer = answerPC.GetStats()
  1406  
  1407  	connStatsOffer = getConnectionStats(t, reportPCOffer, offerPC)
  1408  	assert.Equal(t, uint32(1), connStatsOffer.DataChannelsOpened)
  1409  	assert.Equal(t, uint32(1), connStatsOffer.DataChannelsClosed)
  1410  	assert.Equal(t, uint32(1), connStatsOffer.DataChannelsRequested)
  1411  	assert.Equal(t, uint32(0), connStatsOffer.DataChannelsAccepted)
  1412  	dcStatsOffer = getDataChannelStats(t, reportPCOffer, offerDC)
  1413  	assert.Equal(t, DataChannelStateClosed, dcStatsOffer.State)
  1414  
  1415  	connStatsAnswer = getConnectionStats(t, reportPCAnswer, answerPC)
  1416  	assert.Equal(t, uint32(1), connStatsAnswer.DataChannelsOpened)
  1417  	assert.Equal(t, uint32(1), connStatsAnswer.DataChannelsClosed)
  1418  	assert.Equal(t, uint32(0), connStatsAnswer.DataChannelsRequested)
  1419  	assert.Equal(t, uint32(1), connStatsAnswer.DataChannelsAccepted)
  1420  	dcStatsAnswer = getDataChannelStats(t, reportPCAnswer, answerDC)
  1421  	assert.Equal(t, DataChannelStateClosed, dcStatsAnswer.State)
  1422  
  1423  	answerICETransportStats := getTransportStats(t, reportPCAnswer, "iceTransport")
  1424  	offerICETransportStats := getTransportStats(t, reportPCOffer, "iceTransport")
  1425  	assert.GreaterOrEqual(t, offerICETransportStats.BytesSent, answerICETransportStats.BytesReceived)
  1426  	assert.GreaterOrEqual(t, answerICETransportStats.BytesSent, offerICETransportStats.BytesReceived)
  1427  
  1428  	answerSCTPTransportStats := getSctpTransportStats(t, reportPCAnswer)
  1429  	offerSCTPTransportStats := getSctpTransportStats(t, reportPCOffer)
  1430  	assert.GreaterOrEqual(t, offerSCTPTransportStats.BytesSent, answerSCTPTransportStats.BytesReceived)
  1431  	assert.GreaterOrEqual(t, answerSCTPTransportStats.BytesSent, offerSCTPTransportStats.BytesReceived)
  1432  
  1433  	certificates := offerPC.configuration.Certificates
  1434  
  1435  	for i := range certificates {
  1436  		assert.NotEmpty(t, getCertificateStats(t, reportPCOffer, &certificates[i]))
  1437  	}
  1438  
  1439  	closePairNow(t, offerPC, answerPC)
  1440  }
  1441  
  1442  func TestPeerConnection_GetStats_Closed(t *testing.T) {
  1443  	pc, err := NewPeerConnection(Configuration{})
  1444  	assert.NoError(t, err)
  1445  
  1446  	assert.NoError(t, pc.Close())
  1447  
  1448  	pc.GetStats()
  1449  }