5.5.0 • Published 7 months ago

webrtcmetrics v5.5.0

Weekly downloads
4
License
MIT
Repository
github
Last release
7 months ago

WEBRTC METRICS & STATS

WebRTCMetrics is a JavaScript library that aggregates stats received from several RTCPeerConnection objects and generates JSON reports in live during a call as well as a Call Detail Records (CDR) at the end of the call resuming the main statistics captured.

WebRTCMetrics is based on the WebRTC getStats API and collect statistics using probes. Each probe is associated to a RTCPeerConnection. A probe collects statistics from all streams of a RTCPeerConnection.

Install

Using NPM

$ npm install webrtcmetrics

Using Yarn

$ yarn add webrtcmetrics

Usage

Create a new instance

A new instance of the WebRTCMetrics is created when calling the constructor. A JSON configuration can be set to define the main characteristics of the collect of the statistics.

import WebRTCMetrics from "webrtcmetrics";

// Define your configuration
const configuration = {
  refreshEvery: 3000,   // Optional. Refresh every 3 seconds
  startAfter: 5000,     // Optional. Start collecting stats after 5 seconds
  stopAfter: 30000,     // Optional. Stop collecting stats after 30 seconds
  verbose: false,        // Optional. Display verbose logs or not
  silent: true,           // Optional. No log at all if set to true
};

const metrics = new WebRTCMetrics(configuration);

As defined in that sample, the following parameters can be configured:

  • refreshEvery: Number. Contains the duration to wait (in milliseconds) before collecting a new set of statistics. Default value is 2000.

  • startAfter: Number. Contains the duration to wait (in milliseconds) before collecting the first set of statistics. Default value is equals to 0 for starting immediately.

  • stopAfter: Number. Contains the duration to wait (in milliseconds) before stopping to collect the statistics. This duration starts after the startAfter duration. Default value is -1 which means that the statistics are collected until the function stop() is called.

  • verbose: Boolean. True for displaying verbose information in the logger such as the raw statistics coming from getStats. Default is false.

  • silent: Boolean. True to disable logs. When silent is set to true, the parameter verbose becomes obsolete.

Note: The configuration parameter is optional.

Create a new probe

A probe collects the statistics associated to a RTCPeerConnection.

To create a new probe, call the function createProbe() with a RTCPeerConnection instance to capture.

import WebRTCMetrics from "webrtcmetrics";

// Should exist somewhere in your code
const existingPeerConnection = new RTCPeerConnection(config);

// Initialize the analyzer
const metrics = new WebRTCMetrics();

const probe = metrics.createProbe(existingPeerConnection, {
  pname: 'PeerConnection_1',  // Optional. Name of the peer connection
  cid: 'call007984',          // Optional. Call Id
  uid: 'jdoe@mycorp.com',     // Optional. User Id
  ticket: true,               // Optional. Generate a ticket at the end of the call or not.
  record: true,               // Optional. Record reports in the ticket or not. 
  passthrough: { "inbound-rtp": ["audioLevel"] }   // Optional. Get any properties from the reports
});

Note: The RTCPeerConnection parameter is mandatory whereas the configuration parameter is optional.

createProbe(peerConnection
:
RTCPeerConnection, configuration ? : Object
):
Probe

The configuration parameter contains the following properties:

  • pname: String. Contains the name of the RTCPeerConnection. This is an arbitrary name that can be used to identify statistics received.

  • cid: String. Contains the identifier of the call. This is an arbitrary name that can be used to gather the statistics.

  • uid: String. Contains the identifier of the user. This is an arbitrary name that can be used to gather the statistics.

  • ticket: Boolean. True for generating a ticket when the collect of statistics is stopped. Default is true.

  • record: Boolean. True to link all reports generated to the ticket. This allows to access to all reports individually after the call. Default is false.

Probe lifecycle

Once a probe has been created, it is ready to collect the statistics using reports. The application needs to listen to the event onreport to receive them.

After the call, a ticket that summarizes all the reports received for a probe can be received by listening to the event onticket. Don't forget to put the property ticket to true in the configuration of the WebRTCMetrics Object.

Complete example

const probe = metrics.createProbe(existingPeerConnection, {
  pname: 'PeerConnection_1',  // Optional. Name of the peer connection
  cid: 'call007984',          // Optional. Call Id
  uid: 'jdoe@mycorp.com',     // Optional. User Id
  ticket: true,               // Optional. Generate a ticket at the end of the call or not.
  record: true,               // Optional. Record reports in the ticket or not. 
});

probe.onreport = (report) => {
  // Do something with a report collected (JSON)
};

probe.onticket = (ticket) => {
  // Do something with the ticket collected at the end of the call (JSON)
};

metrics.onresult = (result) => {
  // Do something with the global report collected (JSON)
}

// Start collecting statistics
metrics.startAllProbes();

// At any time, call ID and user ID can be updated
probe.updateUserId('newUserID');
probe.updateCallId('newCallID');

// Once the call is finished, stop the analyzer when running
if (metrics.running) {
  metrics.stopAllProbes();
}

Additional information

The reports can be obtained by registering to event onreport; this callback is called in loop with an interval equals to the value of the refreshEvery parameter and with the report generated.

If you don't want to capture the first curve of statistics but something much more linear, you can specify a delay before receiving the metrics. By default, the stats are captured immediately. But depending on your needs, use the parameter startAfter to delay the capture.

Stats can be captured during a defined period or time. To do that, set a value to the parameter stopAfter to stop receiving reports after that duration given in ms. If you want to capture statistics as long as the call is running, omit that parameter of set the value to -1. In that case, you will have to call manually the method stop() of the probe to stop the collector.

The first set of statistics collected (first report) is called the reference report. It is reported separately from the others (can't be received in the onreport event) but is used for computing statistics of the next ones (for example delta_packets_received).

Note: The report and ticket parameters received from the events are JSON objects. See below for the content.

Dealing with multiple streams in a probe

A RTCPeerConnection can transport more than one audio and video streams (MediaStreamTrack). Statistics is collected per type of stream (audio or video) and per direction (inbound or outbound).

Each report contains the statistics of all streams in live. Ticket summarizes the statistics of all streams at the end of the call.

Creating multiples probes

When connecting to a conference server such as an SFU, you can receive multiple RTCPeerConnection objects. You can collect statistics from each by creating as many probes as needed. One for each RTCPeerConnection.

As the parameter refreshEvery, startAfter and stopAfter are common to all probes created, the statistics of all probes are collected one after the other, as soon as possible in order to be able to compare. To avoid any mistake, each probe has its own timestamp when the stats have been collected.

const probe1 = metrics.createProbe(pc1, {
  pname: 'pc_1',  // Optional. Name of the peer connection
  ticket: true,               // Optional. Generate a ticket at the end of the call or not.
  record: true,               // Optional. Record reports in the ticket or not. 
});

const probe2 = metrics.createProbe(pc2, {
  pname: 'pc_2',  // Optional. Name of the peer connection
  ticket: true,               // Optional. Generate a ticket at the end of the call or not.
  record: true,               // Optional. Record reports in the ticket or not. 
});

probe1.onticket = (result) => {
  // Do something with the ticket of probe 1
}

probe2.onticket = (result) => {
  // Do something with the ticket of probe 2
}

// Start all registered probes
metrics.startAllProbes();

Collecting stats from all probes

Register to the event onresult from the metrics Object created to get a global report that contains all probes reports as well as some global stats.

Note: This method is equivalent to register to the event onreport on each probe individually.

Report Statistics

Each report collected from the event onreport contains the following statistics.

Global statistics

NameValueDescription
pnameStringName of the Peer Connection given
call_idStringIdentifier or abstract name representing the call
user_idStringIdentifier or abstract name representing the user
timestampNumberTimestamp of the metric collected
countNumberNumber of the report

Audio statistics

Audio statistics are gathered under the audio properties which is an object containing all the audio streams collected (inbound and outbound). Each stream is identified by its ssrc.

Each inbound audio stream contains the following statistics:

NameValueDescription
codec_inJSONDescription of the audio input codec and parameters used
codec_id_inStringID of the audio input codec used
delta_KBytes_inNumberNumber of kilobytes (KB) received since the last report
delta_kbs_inNumberNumber of kilobits received per second since the last report
delta_jitter_ms_inNumberIncoming Jitter (in ms)
delta_packets_lost_inNumberNumber of packets lost (not received) since last report
delta_packets_inNumberNumber of packets received since the last report
delta_rtt_ms_inNumberRound Trip-Time (in ms). Could be null when no value collected.
delta_synthetized_ms_inNumberDuration of synthetized voice since last report (in ms)
delta_playout_delay_ms_inNumberDelay of the playout path since last report (in ms)
delta_jitter_buffer_delay_ms_inNumberAverage Jitter buffer delay (in ms)
directionStringDirection of the stream. "inbound" here.
level_inNumberLevel of the input sound. Detect presence of incoming sound
mos_emodel_inNumberAudio quality indicator based on 'MOS E-Model ITU-T G.107.2 (Fullband E-model)'
mos_inNumberAudio quality indicator based on 'Effective Latency' or 'Codec fitting parameters'
percent_packets_lost_inNumberPercent of audio packet lost (not received) since the last report
percent_synthetized_inNumberPercent of voice packet synthetized (generated) since the last report
timestamp_inNumberTimestamp when report has been sent. Associated with delta_rtt_ms_in, total_rtt_measure_in and total_rtt_ms_in
total_KBytes_inNumberNumber of kilobytes (KB) received since the beginning of the call
total_packets_lost_inNumberNumber of packets lost (not received) since the beginning of the call
total_packets_inNumberNumber of packets received since the beginning of the call
total_rtt_measure_inNumberNumber of RTT measurements done
total_rtt_ms_inNumberTotal Round Trip Time since the beginning of the call
total_playout_ms_inNumberTotal duration of the playout since the beginning of the call (in ms)
total_synthetized_ms_inNumberTotal duration of the synthetized voice since the beginning of the call (in ms)
total_percent_synthetized_inNumberPercent of voice packet synthetized (generated) since the beginning of the call
total_time_jitter_buffer_delay_inNumberTotal time spent by all audio samples in jitter buffer (in ms)
total_jitter_emitted_inNumberTotal number of audio samples that have come out the jitter buffer (in ms)
track_inStringThe id of the associated mediastream track

Note: mos_emodel_in and mos_in reflects the quality of the audio media received using a rank from 0 (inaudible) to 4.5 (excellent). It is the quality the local user experienced from his call.

Each outbound audio stream contains the following statistics

NameValueDescription
active_outBooleanTrue if that stream is active (sending media)
codec_outJSONDescription of the audio output codec and parameters used
codec_id_outStringID of the audio output codec used
delta_packet_delay_ms_outNumberAverage duration spent by packets before being sent (in ms)
delta_KBytes_outNumberNumber of kilobytes (KB) sent since last report
delta_kbs_outNumberNumber of kbits sent per second since the last report
delta_jitter_ms_outNumberOutgoing Jitter (in ms)
delta_packets_lost_outNumberNumber of packets lost (not received by the recipient) since last report
delta_packets_outNumberNumber of packets sent since the last report
delta_rtt_ms_outNumberRound Trip-Time (in ms). Could be null when no value collected.
directionStringDirection of the stream. "outbound" here.
level_outNumberLevel of the output sound. Detect presence of outgoing sound
mos_emodel_outNumberAudio quality indicator based on 'MOS E-Model ITU-T G.107.2 (Fullband E-model)'
mos_outNumberAudio quality indicator based on 'Effective Latency' or 'Codec fitting parameters'
percent_packets_lost_outNumberPercent of audio packet lost (not received by the recipient) since the last report
timestamp_outNumberTimestamp when report has been received. Associated with delta_jitter_ms_out and delta_rtt_ms_out
total_KBytes_outNumberNumber of kilobytes (KB) sent since the beginning of the call
total_time_packets_delay_outNumberTotal time spent for all packets before being sent (in ms)
total_packets_lost_outNumberNumber of packets lost (not received by the recipient) since the beginning of the call
total_packets_outNumberNumber of packets sent since the beginning of the call
total_rtt_measure_outNumberNumber of RTT measurements done
total_rtt_ms_outNumberTotal Round Trip Time since the beginning of the call
track_outStringThe id of the mediastream track associated
device_outStringThe label of the device associated to the track_out

Note: mos_emodel_out and mos_out reflects the quality of the audio media sent using a rank from 0 (inaudible) to 4.5 (excellent). It is not the quality the remote peer will experience but is a good indicator of the capacity of the local user to send the media to detect a quality issue on the local side

Video statistics

Video statistics are gathered under the video properties which is an object containing all the video streams collected (inbound and outbound). Each stream is identified by its ssrc.

Each inbound video stream contains the following statistics:

NameValueDescription
decoder_inStringDescription of the video decoder used
delta_KBytes_inNumberNumber of kilobytes (KB) received since the last report
delta_kbs_inNumberNumber of kbits received per second since the last report
delta_jitter_ms_inNumberIncoming Jitter (in ms). Could be null when no value collected
delta_glitch_inJSONNumber of freezes and pauses encountered since the last report
delta_decode_frame_ms_inNumberTime needed to decode a frame (in ms)
delta_processing_delay_ms_inNumberTime needed to process a frame (in ms)
delta_assembly_delay_ms_inNumberTime needed to assemble a frame (in ms)
delta_jitter_buffer_delay_ms_inNumberAverage Jitter buffer delay (in ms)
delta_nack_sent_inNumberNack sent since the last report
delta_packets_lost_inNumberNumber of packets lost (not received) since last report
delta_packets_inNumberNumber of packets received since the last report
delta_pli_sent_inNumberPli sent since the last report
codec_inJSONDescription of the video input codec and parameters used
codec_id_inStringID of the video input codec used
size_inNumberSize of the input video (from remote peer) + framerate
percent_packets_lost_inNumberPercent of audio packet lost (not received) since the last report
total_KBytes_inNumberNumber of kilobytes (KB) received since the beginning of the call
total_frames_decoded_inNumberTotal of frames decoded
total_glitch_inJSONNumber of freezes and pauses encountered since the beginning of the call
total_nack_sent_inNumberTotal nack sent since the beginning of the call
total_packets_lost_inNumberNumber of packets lost (not received) since the beginning of the call
total_packets_inNumberNumber of packets received since the beginning of the call
total_pli_sent_inNumberTotal pli sent since the beginning of the call
total_time_decoded_inNumberTotal time used for decoding all frames (in ms)
total_time_processing_delay_inNumberTotal time used for processing all frames (in ms)
total_time_assembly_delay_inNumberTotal time used for assembling all frames (in ms)
total_time_jitter_buffer_delay_inNumberTotal time spent by all frames in jitter buffer (in ms)
total_jitter_emitted_inNumberTotal number of frames that have come out the jitter buffer (in ms)
timestamp_outNumberTimestamp when report has been received. Associated with delta_jitter_ms_out and delta_rtt_ms_out
track_inStringThe id of the mediastream track associated

Each outbound video stream contains the following statistics

NameValueDescription
active_outBooleanTrue if that stream is active (sending media)
codec_outJSONDescription of the video output codec and parameters used
codec_id_outStringID of the video output codec used
delta_packet_delay_ms_outNumberAverage duration spent by packets before being sent (in ms)
delta_KBytes_outNumberNumber of kilobytes (KB) sent since last report
delta_kbs_outNumberNumber of kbits sent per second since the last report
delta_jitter_ms_outNumberOutgoing Jitter (in ms). Could be null when no value collected.
delta_packets_lost_outNumberNumber of packets lost (not received by the recipient) since last report
delta_encode_frame_ms_outNumberTime needed to encode a frame
delta_nack_received_outNumberNack received since the last report
delta_pli_received_outNumberPli received since the last report
delta_rtt_ms_outNumberRound Trip-Time (in ms). Could be null when no value collected.
encoder_outStringDescription of the video encoder used
size_outObjectSize of the output video sent + framerate (could be lower than the size asked)
size_pref_outObjectSize of the output video asked + framerate
percent_packets_lost_outNumberPercent of audio packet lost (not received by the recipient) since the last report
limitation_outObjectObject containing the reason and the durations spent in each state
total_KBytes_outNumberNumber of kilobytes (KB) sent since the beginning of the call
total_time_packets_delay_outNumberTotal time spent for all packets before being sent (in ms)
total_packets_lost_outNumberNumber of packets lost (not received by the recipient) since the beginning of the call
total_frames_encoded_outNumberTotal of frames encoded
total_nack_received_outNumberTotal nack received since the beginning of the call
total_pli_received_outNumberTotal pli received since the beginning of the call
total_rtt_measure_outNumberNumber of RTT measurements done
total_rtt_ms_outNumberTotal Round Trip Time since the beginning of the call
total_time_encoded_outNumberTotal time used for encoding all frames
timestamp_outNumberTimestamp when report has been received. Associated with delta_jitter_ms_out and delta_rtt_ms_out
track_outStringThe id of the mediastream track associated
device_outStringThe label of the device associated to the track_out

Network properties

NameValueDescription
infrastructureNumberInfrastructure level (0: Eth, 3: Wifi, 5: 4G, 10: 3G).(Deprecated)
local_candidate_idStringID of the local candidate used
local_candidate_protocolStringProtocol used (udp, tcp)
local_candidate_relay_protocolStringProtocol used when relayed with TURN (udp, tcp, tls)
local_candidate_typeStringType of candidate used (host, relay, srflx)
remote_candidate_idStringID of the remote candidate used
remote_candidate_protocolStringProtocol used (udp, tcp)
remote_candidate_typeStringType of candidate used (host, relay, srflx)

Data properties

These stats are collected from the candidate-pair stats.

NameValueDescription
delta_KBytes_inNumberNumber of kilobytes (KB) received since the last report (audio+video)
delta_KBytes_outNumberNumber of kilobytes (KB) sent since last report (audio+video)
delta_kbs_bandwidth_inNumberAvailable incoming bitrate in kb/s (audio+video)
delta_kbs_bandwidth_outNumberAvailable outgoing bitrate in kb/s for (audio+video)
delta_kbs_inNumberNumber of kbits received per second since the last report (audio+video)
delta_kbs_outNumberNumber of kbits sent per second since the last report (audio+video)
delta_rtt_connectivity_msNumberRound Trip-Time (in ms) computed from STUN connectivity checks
total_KBytes_inNumberNumber of kilobytes (KB) received since the beginning of the call (audio+video)
total_KBytes_outNumberNumber of kilobytes (KB) sent since the beginning of the call (audio+video)
total_rtt_connectivity_measureNumberNumber of RTT measurements done (from STUN connectivity checks)
total_rtt_connectivity_msNumberTotal Round Trip Time since the beginning of the call (from STUN connectivity checks)

Experimental

These stats are subject to change in the future

NameValueDescription
time_to_measure_msNumberTime (ms) to measure a probe which means the time to collect and the time to compute the stats

Stop reporting

At any time, calling the method stop() stops collecting statistics on that probe. No other reports are received.

Generating a ticket

When calling the method stop() or automatically after a duration equals to stopAfter, a ticket is generated with the most important information collected. This ticket is generated only if the option ticket has not been manually set to false.

To obtain that ticket, subscribe to the event onticket. The callback is fired when the probe is stopped (ie: by calling the method stop()) or after the stopAfter. The callback is called with a JSON parameter corresponding to something like a CDR.

If the option record has been set to true, the ticket contains all the reports generated.

The ticket generated contains the following information:

NameValueDescription
callObjectContains the call_id and the events related to the call
configurationObjectContains some configuration parameters such as the frequency
dataObjectContains the global statistics of the call
detailsObjectContains the list of reports as well as the reference report
endedDateEnd date of the ticket
ssrcObjectContains the list of all statistics for all streams
startedDateStart date of the ticket
uaObjectContains the ua, the pname and the user_id
versionStringThe version of the ticket format

Each SSRC is an object containing the following statistics:

NameValueDescription
directionStringThe direction of the stream. Can be inbound or outbound
typeStringThe type of the stream. Can be audio or video
bitrateObjectmin, max, avg, values and volatility for Bitrate
jitterObjectmin, max, avg, values and volatility for Jitter
lossObjecttotal, min, max, avg, values and volatility for Packets Loss
rttObject(Outbound only) min, max, avg, values and volatility for Round Trip Time
mosObject(Audio only) min, max, avg, values and volatility
trafficObjectmin, max, avg, values and volatility for Traffic
limitationsObject(Video outbound only) bandwidth, cpu, other, none for Limitations (in percent)

PassThrough

WebRTCMetrics allows to capture any properties from the underlying reports generated by the WebRTC stack (aka getStats API).

Basic usage

For doing that, you need to know which report the property belongs to, and use the key passthrough to give it to * *WebRTCMetrics**.

Here is an example for capturing the audio level for any incoming streams as well as for the local source used

probe1 = metrics.createProbe(pc1, {
  pname: 'pc-bob-1',          // Name of the peer connection (Optional)
  cid: 'call007984',          // Call Id (Optional)
  uid: 'Bob',                 // User Id (Optional)
  passthrough: {
    "inbound-rtp": ["audioLevel"],
    "media-source": ["audioLevel"]
  }
});

The result will be added to each report in the following way:

{
  "passthrough": {
    "audioLevel": {
      "inbound-rtp_audio=3691646660": 0.09140293588061159,
      "media-source_audio=4252341231": 0.02352323323412
    }
  }
}

Advanced usage (units)

Starting version v5.4 you use some tags for collecting the property directly using the right unit.

For that, you can use tag ms for using milliseconds and kbits instead of having bytes.

probe1 = metrics.createProbe(pc1, {
  pname: 'pc-bob-1',          // Name of the peer connection (Optional)
  cid: 'call007984',          // Call Id (Optional)
  uid: 'Bob',                 // User Id (Optional)
  passthrough: {
    "inbound-rtp": ["bytesReceived.kbits"],
    "remote-inbound": ["jitter.ms"]
  }
});

Some metrics are cumulative, if you want to have a value per second, you can use the tag ps.

probe1 = metrics.createProbe(pc1, {
  pname: 'pc-bob-1',          // Name of the peer connection (Optional)
  cid: 'call007984',          // Call Id (Optional)
  uid: 'Bob',                 // User Id (Optional)
  passthrough: {
    "inbound-rtp": ["ps:bytesReceived.kbits"],
    "remote-inbound": ["jitter.ms"]
  }
});

Advanced usage (computation)

Starting version 5.4, you can do computation with the properties collected

For that, you have to specify the properties to used and the operand.

probe1 = metrics.createProbe(pc1, {
  pname: 'pc-bob-1',          // Name of the peer connection (Optional)
  cid: 'call007984',          // Call Id (Optional)
  uid: 'Bob',                 // User Id (Optional)
  passthrough: {
    "remote-inbound-rtp": [
      "[totalRoundTripTime/roundTripTimeMeasurements]"
    ],
    "inbound-rtp": [
      "[framesDecoded-keyFramesDecoded]",
      "[totalDecodeTime/framesDecoded]",
      "[pauseCount+freezeCount]",
      "[totalFreezesDuration+totalPausesDuration]"
    ]
  }
});

The following operands are supported: /, +, -, *. But only one kind of operand can be used in a formula. You can have more than 2 properties in a formula.

Additional information

Callbacks

Setting the onreport, onticket and onresult to null, unregisters the callback previously registered.

Probes

You can get the list of available probes by using the probes accessor.

import WebRTCMetrics from "webrtcmetrics";

const metrics = new WebRTCMetrics();

metrics.createProbe(firstPeerConnection);
metrics.createProbe(secondPeerConnection);

// Get the list of existing probes
const probes = metrics.probes;

Probes can be started and stopped all together.

import WebRTCMetrics from "webrtcmetrics";

const metrics = new WebRTCMetrics();

metrics.createProbe(firstPeerConnection);
metrics.createProbe(secondPeerConnection);

// Start all probes
metrics.startAllProbes();

// Stop all probes
metrics.stopAllProbes();

Events and custom events

Each probe records some WebRTC events related to the RTCPeerConnection or to the devices used. These events are collected and available in the ticket report.

Additionally, to these events, custom events can be recorded too.

import WebRTCMetrics from "webrtcmetrics";

const metrics = new WebRTCMetrics();

const probe = metrics.createProbe(firstPeerConnection);

// ssrc is optional but can be used to link events together. Null by default.
const ssrc = null;

// Data can be any Object
const data = { custom: "data" };

// At any time, for storing an event
probe.addCustomEvent('an event', 'a category', 'a description of the event', new Date(), ssrc, { custom: "data" });

// At any time, for storing a period
probe.addCustomEvent('an event', 'a category', 'a description of the event', new Date(), ssrc, { custom: "data" }, new Date());

Setting the logs level

Logs level can be set in two different ways:

  • When initializing the library and by using the verbose flag from the configuration object.

  • By using the method setupLogLevel

import WebRTCMetrics from "webrtcmetrics";

const metrics = new WebRTCMetrics();
metrics.setupLogLevel('SILENT');
5.5.0

7 months ago

5.4.1

8 months ago

5.4.0

10 months ago

5.3.3

1 year ago

5.3.2

1 year ago

5.3.1

1 year ago

5.3.0

1 year ago

5.2.0

2 years ago

5.1.0

2 years ago

5.0.3

2 years ago

5.0.2

2 years ago

5.0.1

2 years ago

5.0.0

2 years ago

4.0.1

2 years ago

2.0.1

2 years ago

3.2.0

2 years ago

3.1.0

2 years ago

3.0.0

2 years ago

4.0.0

2 years ago

2.0.0

2 years ago

1.4.1

3 years ago

1.4.0

3 years ago

1.3.1

3 years ago

1.3.0

3 years ago

1.2.4

3 years ago

1.2.3

3 years ago

1.2.2

3 years ago

1.2.1

3 years ago

1.2.0

3 years ago

1.1.1

3 years ago

1.1.4

3 years ago

1.1.3

3 years ago

1.1.2

3 years ago

1.1.0

3 years ago

1.0.4

3 years ago

1.0.3

3 years ago

1.0.2

3 years ago

1.0.1

3 years ago

1.0.0

4 years ago