
Unlocking Live Streaming Analytics: Amazon IVS and Datadog Integration - Part 1
Learn how to integrate playback metrics derived from the Amazon IVS Player with Datadog
- Operational Analytics: Proactively monitor stream health in real-time to identify and address playback issues before they impact viewers. For instance, if video startup latency, or the time it takes to deliver the first frame of video to a viewer, spikes above historical averages, you can promptly investigate and address the issue to prevent potential viewer drop-off.
- Playback Health Analytics: Reactively investigate viewer-reported issues and diagnose them to keep viewers engaged. For example, a surge in support tickets about network errors can help you pinpoint whether the issue lies with individual viewers or broader network conditions. This information can in turn be provided to your support teams to properly manage viewer expectations about time to resolution.
- Business Analytics: Gain insights into viewer engagement and demographics to inform strategic business decisions. For instance, analyzing viewer locations and peak viewing times can help you tailor live streaming content to your audience. Additionally, understanding whether viewers prefer mobile or desktop devices can shape your content and distribution strategies.
- How to incorporate the Amazon IVS Player for Web SDK into a web application and play a live stream
- How to calculate quality of experience (QoE) metrics using Amazon IVS Player events
- How to incorporate Datadog’s Real User Monitoring (RUM) Browser SDK to send these events to Datadog
- Part 1 - Setting up the Amazon IVS Player for Web SDK
- Part 2 - Incorporating the Datadog RUM Browser SDK
- Part 3 - Calculating key QoE metrics to inform you of what is going on when viewers watch a live stream
- Part 4 - Sending playback and QoE metrics to Datadog
- A RUM Session represents the journey of a user browsing an application. It contains high-level information about the user (browser, device, geo-location), and aggregates all RUM events (Views, Actions like clicks and scrolls, Errors, etc.) collected during the user journey with a unique session.id attribute
- A RUM View event is generated whenever a user visits a page within an application. While the user remains on the same page, other events (Actions, Errors, etc.) are linked to the related RUM View with the view.id attribute
- Create
index.html
- Add Amazon IVS Player SDK
<script>
tag for the Amazon IVS Player, which will add an IVSPlayer object to the global context. We will then write JavaScript code to initiate the IVS Player and play a live stream.- Add in additional
<script>
tags
config.js
to set some test live streams for playback. The ivs.js
file will contain code to initialize the IVS Player. Finally, the qos_sdk.js
file will contain logic for calculating our quality of experience metrics.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<html>
<head>
<script src="https://player.live-video.net/1.31.0/amazon-ivs-player.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<link rel="stylesheet" href="./css/main.css" />
</head>
<body>
<div id="app">
<div class="inner">
<div class="dropdown-wrapper">
<label class="label-text" for="playback_url"
>Sample Playback Channels</label
>
<select id="playback_url">
<option>Select Channel</option>
</select>
<button type="button" id="playVideo">Click Play</button>
</div>
<div class="spacer"></div>
<div class="player-wrapper">
<div class="aspect-spacer"></div>
<div class="pos-absolute full-width full-height top-0">
<video id="video-player" class="el-player" playsinline></video>
</div>
</div>
</div>
</div>
<script src="./js/config.js"></script>
<script src="./js/qos_sdk.js"></script>
<script src="./js/ivs.js"></script>
</body>
</html>
config.js
to hold our playback URLs. We will programmatically create a dropdown from these.1
2
3
4
5
6
7
const config = {
playbackUrls:
"https://3d26876b73d7.us-west-2.playback.live-video.net/api/video/v1/us-west-2.913157848533.channel.rkCBS9iD1eyd.m3u8, \
https://fcc3ddae59ed.us-west-2.playback.live-video.net/api/video/v1/us-west-2.893648527354.channel.DmumNckWFTqz.m3u8, \
https://fcc3ddae59ed.us-west-2.playback.live-video.net/api/video/v1/us-west-2.893648527354.channel.XFAcAcypUxQm.m3u8, \
https://fcc3ddae59ed.us-west-2.playback.live-video.net/api/video/v1/us-west-2.893648527354.channel.YtnrVcQbttF0.m3u8",
};
ivs.js
to initialize the IVS Player using the IVS Player object. We then attach the IVS player to the <video>
element on our page to display the live stream.config.js
. When one of the playback URLs is selected from the dropdown, it uses the IVS player to load and play the URL.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const videoPlayer = document.getElementById("video-player");
(function (IVSPlayer) {
// Initialize player
const player = IVSPlayer.create();
player.attachHTMLVideoElement(videoPlayer);
// Setup stream and play
player.setAutoplay(true);
// === populating the drop down of channels ===
var mySelect = $("#playback_url");
var playbackUrls = config.playbackUrls.split(",");
$.each(playbackUrls, function (val, text) {
mySelect.append(
$("<option></option>")
.val(text)
.html(text.substring(text.lastIndexOf("channel") + 8))
);
});
$("#playVideo").click(function () {
var selectedPlaybackUrl = $("#playback_url option:selected").val();
console.log("PlaybackURL :", selectedPlaybackUrl);
player.pause();
if (selectedPlaybackUrl) {
player.load(selectedPlaybackUrl);
playbackUrl = selectedPlaybackUrl;
initializeQoS(player, playbackUrl);
player.play();
}
});
})(window.IVSPlayer);
index.html
and we should now see a dropdown to select from one of four test live streams. The drop down displays a unique substring within each playback URL. This unique substring is also a substring that is present in the channel ARN. The channel ARN is a unique identifier for a channel in Amazon IVS, which holds the configuration information for a live stream. By doing this, we can later associate the playback URL to a channel. DD_RUM
to our window object. The DD_RUM
object provides us with methods to initialize the Datadog RUM Browse SDK and send custom data to Datadog.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<html>
<head>
<script src="https://player.live-video.net/1.28.0/amazon-ivs-player.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<link rel="stylesheet" href="./css/main.css" />
<script>
(function (h, o, u, n, d) {
h = h[d] = h[d] || {
q: [],
onReady: function (c) {
h.q.push(c);
},
};
d = o.createElement(u);
d.async = 1;
d.src = n;
n = o.getElementsByTagName(u)[0];
n.parentNode.insertBefore(d, n);
})(
window,
document,
"script",
"https://www.datadoghq-browser-agent.com/us5/v5/datadog-rum.js",
"DD_RUM"
);
window.DD_RUM.onReady(function () {
window.DD_RUM.init({
clientToken: "ADD YOUR DATADOG RUM APP CLIENT TOKEN", // Replace with your Datadog RUM app client token
applicationId: "ADD YOUR DATADOG RUM APP ID", // Replace with your Datadog RUM app ID
// `site` refers to the Datadog site parameter of your organization
// see https://docs.datadoghq.com/getting_started/site/
site: "us1.datadoghq.com",
service: "my-ivs-player-app", // Declare a service name for easier search in Datadog
env: "dev",
// Specify a version number to identify the deployed version of your application in Datadog
version: "1.0.0",
sessionSampleRate: 100,
sessionReplaySampleRate: 0,
startSessionReplayRecordingManually: true,
trackUserInteractions: true,
trackResources: true,
trackLongTasks: true,
defaultPrivacyLevel: "mask-user-input",
allowFallbackToLocalStorage: true,
});
});
</script>
</head>
<body>
<div id="app">
<div class="inner">
<div class="dropdown-wrapper">
<label class="label-text" for="playback_url"
>Sample Playback Channels</label
>
<select id="playback_url">
<option>Select Channel</option>
</select>
<button type="button" id="playVideo">Click Play</button>
</div>
<div class="spacer"></div>
<div class="player-wrapper">
<div class="aspect-spacer"></div>
<div class="pos-absolute full-width full-height top-0">
<video id="video-player" class="el-player" playsinline></video>
</div>
</div>
</div>
</div>
<script src="./js/config.js"></script>
<script src="./js/qos_sdk.js"></script>
<script src="./js/ivs.js"></script>
</body>
</html>
IVS Player Event or State | Description |
---|---|
INITIALIZED | This event is emitted when the player is first created. Used to calculate the time it takes to play the first frame of video in combination with the READY and PLAYING states. Also known as time to video (TTV). |
READY | This state is emitted when the player is ready to start the playback, used to initialize/reset all the tracking variables. |
PLAYING | When the user requests for a channel to be played. The plugin will capture the current playback position, channel watched, startup latency in addition to the user and session ID. |
BUFFERING | This state is emitted during playback when the player’s buffer is not filled in time and playback has stopped. Used to keep track of the number of buffering events during a playback session. |
IDLE | If the previous state was "PLAYING", it calculates the time spent in the playing state since the last update and adds it to the playingTimeMsInLastMinute variable. |
ENDED | If the previous state was "PLAYING", it calculates the time spent in the playing state since the last update and adds it to the playingTimeMsInLastMinute variable. |
ERROR | Keeps a count of the number of errors that occurred in the current playback session. This is sent in the final summary once playback has ended. |
QUALITY_CHANGED | Keeps track of current playback stream quality and emits the metric along with the information on the previous quality level and the newer quality level, rendition name and bitrate and direction in terms of whether the quality moved “UP” or “DOWN” |
- Time to Video is the time it takes for the player to get the first frame of video and play it. The lower the time to video the better. This data point is crucial for engaging users and preventing them from abandoning the video.
- Playback Latency Percentiles measures the distribution of delays between the time a video is sent from Amazon IVS servers and the time it is received and displayed by the Amazon IVS Player. This metric reports the 50th, 75th, 90th, and 95th percentile values of the playback latency distribution, providing insights into the higher end of the latency range experienced by viewers. Unlike an average, these percentile values are not skewed by outliers and better represent the typical and worst-case latency experienced by most viewers.
- Unique Users by Region will show a color coded map of unique users by region. Darker regions indicate more users while lighter regions indicate less users. Hovering over a region will show a popup of the number of users viewing from that region. The Datadog RUM Browser SDK collects the end viewer’s country by default among other default attributes.
- Unique Users by Rendition provides insights into the viewing experience of your live stream audience. It tracks the number of distinct users watching at each available video quality or "rendition" of the stream. This metric can help us understand the distribution of users across different bitrates and resolutions.
- Errors Per Minute Per Channel is the number of errors that occur during video playback per minute of watched video by Amazon IVS channel. The IVS Player emits three possible error types.
- Rebuffering Percentage measures the proportion of a viewer's total watch time that was spent waiting for video segments to download. This metric captures the average amount of time a viewer had to pause playback and wait for the video stream to buffer, relative to their total viewing time. A higher rebuffering percentage indicates more interruptions and a poorer viewing experience, as viewers had to spend a greater fraction of their session waiting for the video to resume playing.
- Average playback time measures the average duration viewers spend watching the live stream, providing insights into audience engagement and content popularity.
- Unique users is the number of distinct users that have accessed the live stream, providing insights into the overall audience size and reach of the content. To achieve this, recall that the data we sent to Datadog in the previous article was enriched with a custom generated unique user ID.
- Unique User Count Per Top 10 Channels tracks the number of distinct users that have accessed the live stream on a specific Amazon IVS channel for the top 10 channels. This metric helps provide insights into the overall audience size and engagement for a particular channel.
- Unique Users by Client Platform tracks the number of distinct users viewing the live stream, categorized by the operating system or device type (e.g. iOS, Android, web browser). This will help us understand the distribution of our audience across different client platforms, allowing us to prioritize features that would enhance the viewing experience for the most popular platforms.
- Percent Time Buffering by Channel Watched measures the percentage of time viewers spend waiting for the video to buffer during playback, broken down by the specific channel they are watching. This metric helps identify any issues with video delivery or encoding that may be causing increased buffering for viewers on certain channels, allowing you to troubleshoot and optimize the experience.
window.DD_RUM
. Recall earlier that when we added the <script>
tag for Datadog RUM Browser SDK, it added a global variable named DD_RUM
to our window object. Datadog lets us instrument custom events, with a lot of flexibility to add all the metadata we need, so the ddRUM
object will later be used to attach these metrics to events and send them off to Datadog.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const ddRum = window.DD_RUM;
// === Define and initialize QoS event work variables ===
// timing control and auxiliary variables
var hasBeenPlayingVideo = false;
var lastReadyTime = -1; // milliseconds since Epoch, UTC, for computing startupLatencyMsOfThisSession
var lastInitializedTime = -1; // milliseconds since Epoch, UTC, for computing timeToVideoMs
var lastPlayerState = "";
var lastPlayerStateUpdateOrPlaybackSummaryEventSentTime = -1; // milliseconds since Epoch, UTC, for computing playing/bufferingTimeMsInLastMinute
var lastPlaybackStartOrPlaybackSummaryEventSentTime = -1; // milliseconds since Epoch, UTC, for the timing of sending playback summary events
// payload of events
var userId = ""; // unique UUID of each device if localStorage is supported, otherwise set to sessionId of each playback session
var sessionId = ""; // unique UUID of each playback session
var startupLatencyMsOfThisSession = 0;
var timeToVideoMs = 0;
var playingTimeMsInLastMinute = 0;
var bufferingTimeMsInLastMinute = 0;
var bufferingCountInLastMinute = 0;
var lastQuality = undefined; // the latest rendition being played
// === Define and initialize QoS event work variables ===
// store whether this is a live channel
var isLive = false;
// store the channel name
var channelWatched = "";
initializeQoS
, which will be the main entry point in our code. This function attaches event listeners to the IVS Player for all of the events listed in the table earlier. The initializeQoS
function also contains a function setInterval()
, which will send playback summary events every minute. Once a playback summary is sent, it resets the QoE variables so that they can be calculated for the next minute.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
function initializeQoS(player, playbackUrl) {
console.log("Initializing...:%s", playbackUrl);
const PlayerState = window.IVSPlayer.PlayerState;
const PlayerEventType = window.IVSPlayer.PlayerEventType;
channelWatched = getChannelWatched(playbackUrl);
console.log("Player...:%j", PlayerState);
// Attach event (player state) listeners
player.addEventListener(PlayerEventType.INITIALIZED, function (err) {
console.log("Player Event - INITIALIZED");
lastInitializedTime = Date.now();
});
player.addEventListener(PlayerState.READY, function () {
console.log("Player State - READY1");
// === Send off playback end event and reset QoS event work variables ===
if (hasBeenPlayingVideo) {
sendOffLastPlaybackSummaryEvent(player);
}
hasBeenPlayingVideo = true;
lastReadyTime = Date.now();
setPlayerStateVariables("READY");
setUserIDSessionID(player);
startupLatencyMsOfThisSession = 0;
playingTimeMsInLastMinute = 0;
bufferingTimeMsInLastMinute = 0;
bufferingCountInLastMinute = 0;
errorCountInLastMinute = 0;
lastQuality = undefined;
// === Send off playback end event and reset QoS event work variables ===
});
player.addEventListener(PlayerState.BUFFERING, function () {
console.log("Player State - BUFFERING");
// === Update QoS event work variables ===
if (lastPlayerState == "PLAYING") {
// PLAYING -> BUFFERING (can only happen in the middle of a playback session)
playingTimeMsInLastMinute +=
Date.now() - lastPlayerStateUpdateOrPlaybackSummaryEventSentTime;
bufferingCountInLastMinute += 1;
}
setPlayerStateVariables("BUFFERING");
// === Update QoS event work variables ===
});
player.addEventListener(PlayerState.PLAYING, function () {
console.log("Player State - PLAYING");
if (startupLatencyMsOfThisSession == 0) {
// the very beginning of a playback session
lastPlaybackStartOrPlaybackSummaryEventSentTime = Date.now();
startupLatencyMsOfThisSession = Date.now() - lastReadyTime;
console.log(
"startupLatencyMsOfThisSession: " + startupLatencyMsOfThisSession
);
timeToVideoMs = Date.now() - lastInitializedTime;
console.log("timeToVideoMs: " + timeToVideoMs);
if (lastQuality === undefined) {
lastQuality = player.getQuality();
}
} else {
if (lastPlayerState == "BUFFERING") {
// BUFFERING -> PLAYING (in the middle of a playback session)
bufferingTimeMsInLastMinute +=
Date.now() - lastPlayerStateUpdateOrPlaybackSummaryEventSentTime;
}
}
setPlayerStateVariables("PLAYING");
// === Send off playback start event and update QoS event work variables ===
});
player.addEventListener(PlayerState.IDLE, function () {
console.log("Player State - IDLE");
// === Update QoS event work variables ===
if (lastPlayerState == "PLAYING") {
// PLAYING -> IDLE
playingTimeMsInLastMinute +=
Date.now() - lastPlayerStateUpdateOrPlaybackSummaryEventSentTime;
} else if (lastPlayerState == "BUFFERING") {
// BUFFERING -> IDLE
bufferingTimeMsInLastMinute +=
Date.now() - lastPlayerStateUpdateOrPlaybackSummaryEventSentTime;
}
setPlayerStateVariables("IDLE");
// === Update QoS event work variables ===
});
player.addEventListener(PlayerState.ENDED, function () {
console.log("Player State - ENDED");
// === Update QoS event work variables ===
if (lastPlayerState == "PLAYING") {
// PLAYING -> ENDED
playingTimeMsInLastMinute +=
Date.now() - lastPlayerStateUpdateOrPlaybackSummaryEventSentTime;
}
setPlayerStateVariables("ENDED");
// === Update QoS event work variables ===
});
// Attach event (error) listeners
player.addEventListener(PlayerEventType.ERROR, function (err) {
console.warn("Player Event - ERROR:", err);
// === Update QoS event work variables ===
const errorLog = new Error(
'{"code":"' +
err.code +
'", "source":"' +
err.source +
'", "message":"' +
err.message +
'"}'
);
errorLog.name = err.type;
errorCountInLastMinute += 1;
window.DD_RUM.addError(errorLog);
// === Update QoS event work variables ===
});
// Attach event (quality changed) listeners
player.addEventListener(PlayerEventType.QUALITY_CHANGED, function () {
log("PlayerEventType - QUALITY_CHANGED");
// === Send off quality change event and update QoS event work variables ===
let newQuality = player.getQuality();
if (lastQuality === undefined) {
lastQuality = newQuality;
log(`Quality initialized to "${lastQuality.name}".`);
} else if (lastQuality.bitrate != newQuality.bitrate) {
log(
`Quality changed from "${lastQuality.name}" to "${newQuality.name}".`
);
sendQualityChangedEvent(lastQuality, newQuality);
lastQuality = newQuality;
}
// === Send off quality change event and update QoS event work variables ===
});
// === Send off a QoS event every minute ===
setInterval(function () {
if (
lastPlaybackStartOrPlaybackSummaryEventSentTime != -1 &&
Date.now() - lastPlaybackStartOrPlaybackSummaryEventSentTime > 60000
) {
sendPlaybackSummaryEventIfNecessary(player);
// Reset work variables
lastPlayerStateUpdateOrPlaybackSummaryEventSentTime =
lastPlaybackStartOrPlaybackSummaryEventSentTime = Date.now();
playingTimeMsInLastMinute = 0;
bufferingTimeMsInLastMinute = 0;
bufferingCountInLastMinute = 0;
errorCountInLastMinute = 0;
}
}, 1000);
}
// === Send off a QoS event every minute ===
PlayerState
and PlayerEventType
variables to make tracking the IVS Player states and events easier.1
2
3
4
5
6
console.log("Initializing...:%s", playbackUrl);
const PlayerState = window.IVSPlayer.PlayerState;
const PlayerEventType = window.IVSPlayer.PlayerEventType;
channelWatched = getChannelWatched(playbackUrl);
console.log("Player...:%j", PlayerState);
getChannelWatched()
. The implementation for this function is as follows and should be placed outside the initializeQoS()
function.1
2
3
4
5
6
7
8
function getChannelWatched(playbackUrl) {
var myIndex1 = playbackUrl.indexOf("channel.") + 8;
var myIndex2 = playbackUrl.indexOf(".m3u8");
var channelName = playbackUrl.substring(myIndex1, myIndex2);
console.log("playbackUrl ", playbackUrl);
console.log("Channel name :", channelName);
return channelName;
}
1
2
3
4
player.addEventListener(PlayerEventType.INITIALIZED, function (err) {
console.log("Player Event - INITIALIZED");
lastInitializedTime = Date.now();
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Attach event (player state) listeners
player.addEventListener(PlayerState.READY, function () {
console.log("Player State - READY1");
// === Send off playback end event and reset QoS event work variables ===
if (hasBeenPlayingVideo) {
sendOffLastPlaybackSummaryEvent(player);
}
hasBeenPlayingVideo = true;
lastReadyTime = Date.now();
setPlayerStateVariables("READY");
setUserIDSessionID(player);
startupLatencyMsOfThisSession = 0;
playingTimeMsInLastMinute = 0;
bufferingTimeMsInLastMinute = 0;
bufferingCountInLastMinute = 0;
errorCountInLastMinute = 0;
lastQuality = undefined;
// === Send off playback end event and reset QoS event work variables ===
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
player.addEventListener(PlayerState.BUFFERING, function () {
console.log("Player State - BUFFERING");
// === Update QoS event work variables ===
if (lastPlayerState == "PLAYING") {
// PLAYING -> BUFFERING (can only happen in the middle of a playback session)
playingTimeMsInLastMinute +=
Date.now() - lastPlayerStateUpdateOrPlaybackSummaryEventSentTime;
bufferingCountInLastMinute += 1;
}
setPlayerStateVariables("BUFFERING");
// === Update QoS event work variables ===
});
setPlayerStateVariables
. We call this function to update the variable used to track the player’s last state and the timestamp when this function was last called. Doing this will be useful in the calculation of the other QoE metrics as you will see soon. Here’s the implementation for this function. This utility function should be placed outside of the initializeQoS
function.1
2
3
4
function setPlayerStateVariables(myPlayerState) {
lastPlayerState = myPlayerState;
lastPlayerStateUpdateOrPlaybackSummaryEventSentTime = Date.now();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
player.addEventListener(PlayerState.PLAYING, function () {
console.log("Player State - PLAYING");
if (startupLatencyMsOfThisSession == 0) {
// the very beginning of a playback session
lastPlaybackStartOrPlaybackSummaryEventSentTime = Date.now();
startupLatencyMsOfThisSession = Date.now() - lastReadyTime;
console.log(
"startupLatencyMsOfThisSession: " + startupLatencyMsOfThisSession
);
timeToVideoMs = Date.now() - lastInitializedTime;
console.log("timeToVideoMs: " + timeToVideoMs);
// sendPlaybackStartEvent(player);
if (lastQuality === undefined) {
lastQuality = player.getQuality();
}
} else {
if (lastPlayerState == "BUFFERING") {
// BUFFERING -> PLAYING (in the middle of a playback session)
bufferingTimeMsInLastMinute +=
Date.now() - lastPlayerStateUpdateOrPlaybackSummaryEventSentTime;
}
}
setPlayerStateVariables("PLAYING");
// === Send off playback start event and update QoS event work variables ===
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
player.addEventListener(PlayerState.IDLE, function () {
console.log("Player State - IDLE");
// === Update QoS event work variables ===
if (lastPlayerState == "PLAYING") {
// PLAYING -> IDLE
playingTimeMsInLastMinute +=
Date.now() - lastPlayerStateUpdateOrPlaybackSummaryEventSentTime;
} else if (lastPlayerState == "BUFFERING") {
// BUFFERING -> IDLE
bufferingTimeMsInLastMinute +=
Date.now() - lastPlayerStateUpdateOrPlaybackSummaryEventSentTime;
}
setPlayerStateVariables("IDLE");
// === Update QoS event work variables ===
});
1
2
3
4
5
6
7
8
9
10
11
12
13
player.addEventListener(PlayerState.ENDED, function () {
console.log("Player State - ENDED");
// === Update QoS event work variables ===
if (lastPlayerState == "PLAYING") {
// PLAYING -> ENDED
playingTimeMsInLastMinute +=
Date.now() - lastPlayerStateUpdateOrPlaybackSummaryEventSentTime;
}
setPlayerStateVariables("ENDED");
// === Update QoS event work variables ===
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
player.addEventListener(PlayerEventType.ERROR, function (err) {
console.warn("Player Event - ERROR:", err);
// === Update QoS event work variables ===
const errorLog = new Error(
'{"code":"' +
err.code +
'", "source":"' +
err.source +
'", "message":"' +
err.message +
'"}'
);
errorLog.name = err.type;
errorCountInLastMinute += 1;
window.DD_RUM.addError(errorLog);
// === Update QoS event work variables ===
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Attach event (quality changed) listeners
player.addEventListener(PlayerEventType.QUALITY_CHANGED, function () {
log("PlayerEventType - QUALITY_CHANGED");
// === Send off quality change event and update QoS event work variables ===
let newQuality = player.getQuality();
if (lastQuality === undefined) {
lastQuality = newQuality;
log(`Quality initialized to "${lastQuality.name}".`);
} else if (lastQuality.bitrate != newQuality.bitrate) {
log(
`Quality changed from "${lastQuality.name}" to "${newQuality.name}".`
);
sendQualityChangedEvent(lastQuality, newQuality);
lastQuality = newQuality;
}
// === Send off quality change event and update QoS event work variables ===
});
sendQualityChangedEvent()
function. We will cover the implementation of that function in the next section.initializeQoS
function, we send in a custom event a summary containing all the metrics we have just calculated in all our previous event listeners to Datadog. These metrics will be sent from our custom sendPlaybackSummaryEventIfNecessary function. We will show how this function is implemented in the next section. Using the setInterval function, we will send these metrics off to Datadog every minute. Once the metrics are sent, we reset all of our metrics tracking variables to zero so that we can recalculate them for the next minute.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// === Send off a QoS event every minute ===
setInterval(function () {
if (
lastPlaybackStartOrPlaybackSummaryEventSentTime != -1 &&
Date.now() - lastPlaybackStartOrPlaybackSummaryEventSentTime > 60000
) {
sendPlaybackSummaryEventIfNecessary(player);
// Reset work variables
lastPlayerStateUpdateOrPlaybackSummaryEventSentTime =
lastPlaybackStartOrPlaybackSummaryEventSentTime = Date.now();
playingTimeMsInLastMinute = 0;
bufferingTimeMsInLastMinute = 0;
bufferingCountInLastMinute = 0;
errorCountInLastMinute = 0;
}
}, 1000);
sendPlaybackSummaryEventIfNecessary
function within the initializeQoS
function to send the calculated metrics to Datadog. In this sendPlaybackSummaryEventIfNecessary
function, we create a JSON object that contains a summary of all of our QoE metrics. We first make some adjustments to the playing time and buffering time variables, making sure to capture any uncounted time if the IVS Player was in either the playing or buffering state. Additionally, we will include the number of frames that have been dropped so far during the playback session. We can get this using the getDroppedFrames() method exposed by the IVS Player.getLiveLatency
method provided by the IVS Player. Live latency tells us the time it takes for a video frame to be sent IVS to the player. We multiply it by 1,000 since it is returned to us in seconds and we want to display it in milliseconds to be consistent with our other metrics.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Send playback QoS summary (PLAYBACK_SUMMARY) event
function sendPlaybackSummaryEventIfNecessary(player) {
if (lastPlayerState == "PLAYING") {
// collect the uncounted time in the PLAYING state
playingTimeMsInLastMinute +=
Date.now() - lastPlayerStateUpdateOrPlaybackSummaryEventSentTime;
} else if (lastPlayerState == "BUFFERING") {
// Collect the uncounted time in the BUFFERING state
bufferingTimeMsInLastMinute +=
Date.now() - lastPlayerStateUpdateOrPlaybackSummaryEventSentTime;
}
if (playingTimeMsInLastMinute > 0 || bufferingTimeMsInLastMinute > 0) {
var myJson = {};
myJson.metric_type = "PLAYBACK_SUMMARY";
myJson.user_id = userId;
myJson.session_id = sessionId;
myJson.client_platform = "web";
// myJson.is_live = isLive;
myJson.channel_watched = channelWatched;
myJson.playing_time_ms = playingTimeMsInLastMinute;
myJson.buffering_time_ms = bufferingTimeMsInLastMinute;
myJson.buffering_count = bufferingCountInLastMinute;
myJson.error_count = errorCountInLastMinute;
myJson.rendition_name = lastQuality.name;
myJson.rendition_height = lastQuality.height;
myJson.live_latency_ms = Math.round(player.getLiveLatency() * 1000);
myJson.startup_latency_ms = startupLatencyMsOfThisSession;
pushPayload(myJson);
console.log("send QoS event - PlaybackSummary ", JSON.stringify(myJson));
}
}
sessionId
represents a unique identifier for each instance of the IVS Player when it is playing a live stream. If an end user for example has several browser windows open each with a separate instance of the IVS Player playing a live stream, each of these would count as a separate play session and have a unique session ID. The session ID is retrieved using the getSessionId
method from the IVS Player. Associating all of these metrics to a given user ID or session ID will be important for how we graph our metrics in Datadog later.ddRUM
object to set the user ID associated with our RUM session using the setUser
method. This will enable us to more easily query and filter for the user associated with this metrics data in Datadog. We can also optionally add other user attributes using `setUser` to make it easier to filter users based on certain attributes1
2
3
4
5
6
7
8
9
10
11
12
13
14
function setUserIDSessionID(player) {
sessionId = player.getSessionId();
if (typeof Storage !== "undefined") {
if (!localStorage.getItem("ivs_qos_user_id")) {
localStorage.setItem("ivs_qos_user_id", sessionId);
}
userId = localStorage.getItem("ivs_qos_user_id");
} else {
log("Sorry! No web storage support. Use Session ID as User Id");
userId = sessionId;
}
ddRum.setUser({ id: userId });
}
QUALITY_CHANGED
event to Datadog whenever the IVS Player switches to a different rendition (change in quality, bitrate). This payload is sent to Datadog using the custom sendQualityChangedEvent
function.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Send quality (i.e., rendition) change (QUALITY_CHANGE) event
function sendQualityChangedEvent(lastQuality, newQuality) {
var myJson = {};
myJson.metric_type = "QUALITY_CHANGED";
myJson.user_id = userId;
myJson.session_id = sessionId;
myJson.client_platform = config.client_platform;
myJson.channel_watched = channelWatched;
myJson.from_rendition_name = lastQuality.name;
myJson.to_rendition_name = newQuality.name;
myJson.from_bitrate = lastQuality.bitrate;
myJson.to_bitrate = newQuality.bitrate;
myJson.step_direction =
newQuality.bitrate > lastQuality.bitrate ? "UP" : "DOWN";
pushPayload(myJson);
}
pushPayload()
function, which wraps a call to the ddRUM
object to send this JSON payload to Datadog.PLAYBACK_SUMMARY
and QUALITY_CHANGED
. The name PLAYBACK_SUMMARY
is for the JSON object containing all of our calculated QoE metrics in the last minute, while the name QUALITY_CHANGED
is for the JSON object containing the information 1
2
3
4
5
function pushPayload(payload) {
ddRum.onReady(function () {
ddRum.addAction(payload.metric_type, payload);
});
}
index.html
, select a channel to play from a dropdown. If you are using Chrome, also open the developer tools console so we can display debug output. Notice in the console, we print out the player state as it changes, and calculations for the metrics that we will send to Datadog.PLAYBACK_SUMMARY
event was sent to Datadog.Any opinions in this post are those of the individual author and may not reflect the opinions of AWS.