@srgssr/pillarbox-web 1.13.1 → 1.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -110,7 +110,7 @@ function _objectWithoutProperties(source, excluded) {
110
110
  return target;
111
111
  }
112
112
 
113
- var version = "1.13.0";
113
+ var version = "1.14.0";
114
114
 
115
115
  /**
116
116
  * @ignore
@@ -1643,6 +1643,832 @@ class SRGAnalytics {
1643
1643
  }
1644
1644
  }
1645
1645
 
1646
+ /* eslint max-statements: ["error", 25]*/
1647
+
1648
+ /**
1649
+ * The PillarboxMonitoring class retrieves data about media playback.
1650
+ *
1651
+ * This data can be used to :
1652
+ * - help investigate playback problems
1653
+ * - measure the quality of our service
1654
+ *
1655
+ * The sending of this data tries to respect as much as possible the
1656
+ * specification described in the link below.
1657
+ *
1658
+ * However, some platforms may have certain limitations.
1659
+ * In this case, only the data available will be sent.
1660
+ *
1661
+ * @see https://github.com/SRGSSR/pillarbox-documentation/blob/main/Specifications/monitoring.md
1662
+ */
1663
+ class PillarboxMonitoring {
1664
+ constructor(player, {
1665
+ playerName = 'none',
1666
+ playerVersion = 'none',
1667
+ platform = 'Web',
1668
+ schemaVersion = 1,
1669
+ heartbeatInterval = 30000,
1670
+ beaconUrl = 'https://monitoring.pillarbox.ch/api/events'
1671
+ } = {}) {
1672
+ /**
1673
+ * @type {import('video.js/dist/types/player').default}
1674
+ */
1675
+ this.player = player;
1676
+ /**
1677
+ * @type {string}
1678
+ */
1679
+ this.playerName = playerName;
1680
+ /**
1681
+ * @type {string}
1682
+ */
1683
+ this.playerVersion = playerVersion;
1684
+ /**
1685
+ * @type {string}
1686
+ */
1687
+ this.platform = platform;
1688
+ /**
1689
+ * @type {string}
1690
+ */
1691
+ this.schemaVersion = schemaVersion;
1692
+ /**
1693
+ * @type {Number}
1694
+ */
1695
+ this.heartbeatInterval = heartbeatInterval;
1696
+ /**
1697
+ * @type {string}
1698
+ */
1699
+ this.beaconUrl = beaconUrl;
1700
+ /**
1701
+ * @type {string}
1702
+ */
1703
+ this.currentSessionId = undefined;
1704
+ /**
1705
+ * @type {Number}
1706
+ */
1707
+ this.lastPlaybackDuration = 0;
1708
+ /**
1709
+ * @type {Number}
1710
+ */
1711
+ this.lastPlaybackStartTimestamp = 0;
1712
+ /**
1713
+ * @type {Number}
1714
+ */
1715
+ this.lastStallCount = 0;
1716
+ /**
1717
+ * @type {Number}
1718
+ */
1719
+ this.lastStallDuration = 0;
1720
+ /**
1721
+ * @type {Number}
1722
+ */
1723
+ this.loadStartTimestamp = undefined;
1724
+ /**
1725
+ * @type {Number}
1726
+ */
1727
+ this.metadataRequestTime = 0;
1728
+ /**
1729
+ * @type {string}
1730
+ */
1731
+ this.mediaAssetUrl = undefined;
1732
+ /**
1733
+ * @type {string}
1734
+ */
1735
+ this.mediaId = undefined;
1736
+ /**
1737
+ * @type {string}
1738
+ */
1739
+ this.mediaMetadataUrl = undefined;
1740
+ /**
1741
+ * @type {string}
1742
+ */
1743
+ this.mediaOrigin = undefined;
1744
+ /**
1745
+ * @type {Number}
1746
+ */
1747
+ this.tokenRequestTime = 0;
1748
+ this.addListeners();
1749
+ }
1750
+
1751
+ /**
1752
+ * Adds event listeners to the player and the window.
1753
+ */
1754
+ addListeners() {
1755
+ this.bindCallBacks();
1756
+ this.player.on('loadstart', this.loadStart);
1757
+ this.player.on('loadeddata', this.loadedData);
1758
+ this.player.on('playing', this.playbackStart);
1759
+ this.player.on('pause', this.playbackStop);
1760
+ this.player.on('error', this.error);
1761
+ this.player.on(['playerreset', 'dispose', 'ended'], this.sessionStop);
1762
+ this.player.on(['waiting', 'stalled'], this.stalled);
1763
+ window.addEventListener('beforeunload', this.sessionStop);
1764
+ }
1765
+
1766
+ /**
1767
+ * The current bandwidth of the last segment download.
1768
+ *
1769
+ * @returns {number|undefined} The current bandwidth in bits per second,
1770
+ * undefined otherwise.
1771
+ */
1772
+ bandwidth() {
1773
+ const playerStats = this.player.tech(true).vhs ? this.player.tech(true).vhs.stats : undefined;
1774
+ return playerStats ? playerStats.bandwidth : undefined;
1775
+ }
1776
+
1777
+ /**
1778
+ * Binds the callback functions to the current instance.
1779
+ */
1780
+ bindCallBacks() {
1781
+ this.error = this.error.bind(this);
1782
+ this.loadedData = this.loadedData.bind(this);
1783
+ this.loadStart = this.loadStart.bind(this);
1784
+ this.playbackStart = this.playbackStart.bind(this);
1785
+ this.playbackStop = this.playbackStop.bind(this);
1786
+ this.stalled = this.stalled.bind(this);
1787
+ this.sessionStop = this.sessionStop.bind(this);
1788
+ }
1789
+
1790
+ /**
1791
+ * Get the buffer duration in milliseconds.
1792
+ *
1793
+ * @returns {Number} The buffer duration
1794
+ */
1795
+ bufferDuration() {
1796
+ const buffered = this.player.buffered();
1797
+ let bufferDuration = 0;
1798
+ for (let i = 0; i < buffered.length; i++) {
1799
+ const start = buffered.start(i);
1800
+ const end = buffered.end(i);
1801
+ bufferDuration += end - start;
1802
+ }
1803
+ return PillarboxMonitoring.secondsToMilliseconds(bufferDuration);
1804
+ }
1805
+
1806
+ /**
1807
+ * Get the current representation when playing a Dash or Hls media.
1808
+ *
1809
+ * @typedef {Object} Representation
1810
+ * @property {number|undefined} bandwidth The bandwidth of the current
1811
+ * representation
1812
+ * @property {number|undefined} programDateTime The program date time of the
1813
+ * current representation
1814
+ * @property {string|undefined} uri The URL of the current representation
1815
+ *
1816
+ * @returns {Representation|undefined} The current representation object
1817
+ * undefined otherwise
1818
+ */
1819
+ currentRepresentation() {
1820
+ const {
1821
+ activeCues: {
1822
+ cues_: [cue]
1823
+ } = {
1824
+ cues_: []
1825
+ }
1826
+ } = Array.from(this.player.textTracks()).find(({
1827
+ label,
1828
+ kind
1829
+ }) => kind === 'metadata' && label === 'segment-metadata') || {};
1830
+ return cue ? cue.value : undefined;
1831
+ }
1832
+
1833
+ /**
1834
+ * Get the current resource information including bitrate and URL when available.
1835
+ *
1836
+ * @typedef {Object} Resource
1837
+ * @property {number|undefined} bitrate The bitrate of the current resource
1838
+ * @property {string|undefined} url The URL of the current resource
1839
+ *
1840
+ * @returns {Resource} The current resource information.
1841
+ */
1842
+ currentResource() {
1843
+ let {
1844
+ bandwidth: bitrate,
1845
+ uri: url
1846
+ } = this.currentRepresentation() || {};
1847
+ if (pillarbox.browser.IS_ANY_SAFARI) {
1848
+ const {
1849
+ configuration
1850
+ } = Array.from(this.player.videoTracks()).find(track => track.selected) || {};
1851
+ bitrate = configuration ? configuration.bitrate : undefined;
1852
+ url = this.player.currentSource().src;
1853
+ }
1854
+ return {
1855
+ bitrate,
1856
+ url
1857
+ };
1858
+ }
1859
+
1860
+ /**
1861
+ * The media data of the current source.
1862
+ *
1863
+ * @returns {Object} The media data of the current source, or an empty object
1864
+ * if no media data is available.
1865
+ */
1866
+ currentSourceMediaData() {
1867
+ if (!this.player.currentSource().mediaData) return {};
1868
+ return this.player.currentSource().mediaData;
1869
+ }
1870
+
1871
+ /**
1872
+ * Handles player errors by sending an `ERROR` event, then resets the session.
1873
+ */
1874
+ error() {
1875
+ const error = this.player.error();
1876
+ const playbackPosition = this.playbackPosition();
1877
+ const representation = this.currentRepresentation();
1878
+ const url = representation ? representation.uri : this.player.currentSource().src;
1879
+ if (!this.player.hasStarted()) {
1880
+ this.sendEvent('START', this.startEventData());
1881
+ }
1882
+ this.sendEvent('ERROR', _objectSpread2(_objectSpread2({
1883
+ log: JSON.stringify(error.metadata || pillarbox.log.history().slice(-15)),
1884
+ message: error.message,
1885
+ name: error.code
1886
+ }, playbackPosition), {}, {
1887
+ url
1888
+ }));
1889
+ this.reset();
1890
+ }
1891
+
1892
+ /**
1893
+ * Get the DRM license request duration from performance API.
1894
+ *
1895
+ * @returns {number|undefined} The request duration
1896
+ */
1897
+ getDrmRequestDuration() {
1898
+ const keySystems = Object.values(this.player.currentSource().keySystems || {}).map(keySystem => keySystem.url);
1899
+ if (!keySystems.length) return;
1900
+ const resource = performance.getEntriesByType('resource').filter(({
1901
+ initiatorType,
1902
+ name
1903
+ }) => initiatorType === 'xmlhttprequest' && keySystems.includes(name)).pop();
1904
+ return resource && resource.duration;
1905
+ }
1906
+
1907
+ /**
1908
+ * Get metadata information from the performance API for a given id.
1909
+ *
1910
+ * @typedef {Object} MetadataInfo
1911
+ * @property {string} name The URL of the resource
1912
+ * @property {number} duration The duration of the resource fetch in milliseconds
1913
+ *
1914
+ * @param {string} id The id to search for in the resource entries
1915
+ *
1916
+ * @returns {MetadataInfo|undefined} An object containing metadata
1917
+ * information, or undefined otherwise
1918
+ */
1919
+ getMetadataInfo(id) {
1920
+ const resource = performance.getEntriesByType('resource').filter(({
1921
+ initiatorType,
1922
+ name
1923
+ }) => initiatorType === 'fetch' && name.includes(id)).pop();
1924
+ if (!resource) return {};
1925
+ return {
1926
+ name: resource.name,
1927
+ duration: resource.duration
1928
+ };
1929
+ }
1930
+
1931
+ /**
1932
+ * Get the Akamai token request duration from performance API.
1933
+ *
1934
+ * @returns {number|undefined} The request duration
1935
+ */
1936
+ getTokenRequestDuration(tokenType) {
1937
+ if (!tokenType) return;
1938
+ const resource = performance.getEntriesByType('resource').filter(({
1939
+ initiatorType,
1940
+ name
1941
+ }) => initiatorType === 'fetch' && name.includes('/akahd/token')).pop();
1942
+ return resource && resource.duration;
1943
+ }
1944
+
1945
+ /**
1946
+ * Send an 'HEARTBEAT' event with the date of the current playback state at
1947
+ * regular intervals.
1948
+ */
1949
+ heartbeat() {
1950
+ this.heartbeatIntervalId = setInterval(() => {
1951
+ this.sendEvent('HEARTBEAT', this.statusEventData());
1952
+ }, this.heartbeatInterval);
1953
+ }
1954
+
1955
+ /**
1956
+ * Check if the tracker is disabled.
1957
+ *
1958
+ * @returns {Boolean} __true__ if disabled __false__ otherwise.
1959
+ */
1960
+ isTrackerDisabled() {
1961
+ const currentSource = this.player.currentSource();
1962
+ if (!Array.isArray(currentSource.disableTrackers)) {
1963
+ return Boolean(currentSource.disableTrackers);
1964
+ }
1965
+ return Boolean(currentSource.disableTrackers.find(tracker => tracker.toLowerCase() === PillarboxMonitoring.name.toLowerCase()));
1966
+ }
1967
+
1968
+ /**
1969
+ * Handles the session start by sending a `START` event immediately followed
1970
+ * by a `HEARTBEAT` when the `loadeddata` event is triggered.
1971
+ */
1972
+ loadedData() {
1973
+ this.sendEvent('START', this.startEventData());
1974
+ this.sendEvent('HEARTBEAT', this.statusEventData());
1975
+ // starts the heartbeat interval
1976
+ this.heartbeat();
1977
+ }
1978
+
1979
+ /**
1980
+ * Handles `loadstart` event and captures the current timestamp. Will be used
1981
+ * to calculate the media loading time.
1982
+ */
1983
+ loadStart() {
1984
+ // if the content is a plain old URL
1985
+ if (!Object.keys(this.currentSourceMediaData()).length && this.currentSessionId) {
1986
+ this.sessionStop();
1987
+ // Reference timestamp used to calculate the different time metrics.
1988
+ this.sessionStartTimestamp = PillarboxMonitoring.timestamp();
1989
+ }
1990
+ this.loadStartTimestamp = PillarboxMonitoring.timestamp();
1991
+ }
1992
+
1993
+ /**
1994
+ * The media information.
1995
+ *
1996
+ * @typedef {Object} MediaInfo
1997
+ * @property {string} asset_url The URL of the media
1998
+ * @property {string} id The ID of the media
1999
+ * @property {string} metadata_url The URL of the media metadata
2000
+ * @property {string} origin The origin of the media
2001
+ *
2002
+ * @returns {MediaInfo} An object container the media information
2003
+ */
2004
+ mediaInfo() {
2005
+ return {
2006
+ asset_url: this.mediaAssetUrl,
2007
+ id: this.mediaId,
2008
+ metadata_url: this.mediaMetadataUrl,
2009
+ origin: this.mediaOrigin
2010
+ };
2011
+ }
2012
+
2013
+ /**
2014
+ * The total playback duration for the current session.
2015
+ *
2016
+ * @returns {number} The total playback duration in milliseconds.
2017
+ */
2018
+ playbackDuration() {
2019
+ if (!this.lastPlaybackStartTimestamp) {
2020
+ return this.lastPlaybackDuration;
2021
+ }
2022
+ return PillarboxMonitoring.timestamp() + this.lastPlaybackDuration - this.lastPlaybackStartTimestamp;
2023
+ }
2024
+
2025
+ /**
2026
+ * The current playback position and position timestamp.
2027
+ *
2028
+ * @typedef {Object} PlaybackPosition
2029
+ * @property {number} position The current playback position in milliseconds
2030
+ * @property {number|undefined} position_timestamp The timestamp of the
2031
+ * current playback position, or undefined if not available
2032
+ *
2033
+ * @returns {PlaybackPosition} The playback position object.
2034
+ */
2035
+ playbackPosition() {
2036
+ const currentRepresentation = this.currentRepresentation();
2037
+ const position = PillarboxMonitoring.secondsToMilliseconds(this.player.currentTime());
2038
+ let position_timestamp;
2039
+
2040
+ // Get the position timestamp from the program date time when VHS is used
2041
+ // or undefined if there is no value
2042
+ if (currentRepresentation) {
2043
+ position_timestamp = currentRepresentation.programDateTime;
2044
+ }
2045
+
2046
+ // Calculate the position timestamp from the start date on Safari
2047
+ if (pillarbox.browser.IS_ANY_SAFARI) {
2048
+ const startDate = Date.parse(this.player.$('video').getStartDate());
2049
+ position_timestamp = !isNaN(startDate) ? startDate + position : undefined;
2050
+ }
2051
+ return {
2052
+ position,
2053
+ position_timestamp
2054
+ };
2055
+ }
2056
+
2057
+ /**
2058
+ * Assign the timestamp each time the playback starts.
2059
+ */
2060
+ playbackStart() {
2061
+ this.lastPlaybackStartTimestamp = PillarboxMonitoring.timestamp();
2062
+ }
2063
+
2064
+ /**
2065
+ * Calculates and accumulates the duration of the playback session each time
2066
+ * the playback stops for the current media.
2067
+ */
2068
+ playbackStop() {
2069
+ this.lastPlaybackDuration += PillarboxMonitoring.timestamp() - this.lastPlaybackStartTimestamp;
2070
+ this.lastPlaybackStartTimestamp = 0;
2071
+ }
2072
+
2073
+ /**
2074
+ * The current dimensions of the player.
2075
+ *
2076
+ * @typedef {Object} PlayerCurrentDimensions
2077
+ * @property {number} width The current width of the player
2078
+ * @property {number} height The current height of the player
2079
+ *
2080
+ * @returns {PlayerCurrentDimensions} The current dimensions of the player object.
2081
+ */
2082
+ playerCurrentDimensions() {
2083
+ return this.player.currentDimensions();
2084
+ }
2085
+
2086
+ /**
2087
+ * Information about the player.
2088
+ *
2089
+ * @typedef {Object} PlayerInfo
2090
+ * @property {string} name The name of the player
2091
+ * @property {string} version The version of the player
2092
+ * @property {string} platform The platform on which the player is running
2093
+ *
2094
+ * @returns {PlayerInfo} An object containing player information.
2095
+ */
2096
+ playerInfo() {
2097
+ return {
2098
+ name: this.playerName,
2099
+ version: this.playerVersion,
2100
+ platform: this.platform
2101
+ };
2102
+ }
2103
+
2104
+ /**
2105
+ * Generates the QoE timings object.
2106
+ *
2107
+ * @typedef {Object} QoeTimings
2108
+ * @property {number} metadata The time taken to load metadata
2109
+ * @property {number} asset The time taken to load the asset
2110
+ * @property {number} total The total time taken from session start to data load
2111
+ *
2112
+ * @param {number} timeToLoadedData The time taken to load the data
2113
+ * @param {number} timestamp The current timestamp
2114
+ *
2115
+ * @returns {QoeTimings} The QoE timings
2116
+ */
2117
+ qoeTimings(timeToLoadedData, timestamp) {
2118
+ return {
2119
+ metadata: this.metadataRequestTime,
2120
+ asset: timeToLoadedData,
2121
+ total: timestamp - this.sessionStartTimestamp
2122
+ };
2123
+ }
2124
+
2125
+ /**
2126
+ * Generates the QoS timings object.
2127
+ *
2128
+ * @typedef {Object} QosTimings
2129
+ * @property {number} asset The time taken to load the asset
2130
+ * @property {number} drm The time taken for DRM processing
2131
+ * @property {number} metadata The time taken to load metadata
2132
+ * @property {number} token The time taken to request the token
2133
+ *
2134
+ * @param {number} timeToLoadedData The time taken to load the data
2135
+ *
2136
+ * @returns {QosTimings} The QoS timings
2137
+ */
2138
+ qosTimings(timeToLoadedData) {
2139
+ return {
2140
+ asset: timeToLoadedData,
2141
+ drm: this.getDrmRequestDuration(),
2142
+ metadata: this.metadataRequestTime,
2143
+ token: this.tokenRequestTime
2144
+ };
2145
+ }
2146
+
2147
+ /**
2148
+ * Removes all event listeners from the player and the window.
2149
+ */
2150
+ removeListeners() {
2151
+ this.player.off('loadstart', this.loadStart);
2152
+ this.player.off('loadeddata', this.loadedData);
2153
+ this.player.off('playing', this.playbackStart);
2154
+ this.player.off('pause', this.playbackStop);
2155
+ this.player.off('error', this.error);
2156
+ this.player.off(['playerreset', 'dispose', 'ended'], this.sessionStop);
2157
+ this.player.off(['waiting', 'stalled'], this.stalled);
2158
+ window.removeEventListener('beforeunload', this.sessionStop);
2159
+ }
2160
+
2161
+ /**
2162
+ * Remove the token from the asset URL.
2163
+ *
2164
+ * @param {string} assetUrl The URL of the asset
2165
+ *
2166
+ * @returns {string|undefined} The URL without the token, or undefined if the
2167
+ * input URL is invalid
2168
+ */
2169
+ removeTokenFromAssetUrl(assetUrl) {
2170
+ if (!assetUrl) return;
2171
+ try {
2172
+ const url = new URL(assetUrl);
2173
+ url.searchParams.delete('hdnts');
2174
+ return url.href;
2175
+ } catch (e) {
2176
+ return;
2177
+ }
2178
+ }
2179
+
2180
+ /**
2181
+ * Resets the playback session and clears relevant properties.
2182
+ *
2183
+ * @param {Event} event The event that triggered the reset. If the event type
2184
+ * is not 'ended' or 'playerreset', listeners will be removed.
2185
+ */
2186
+ reset(event) {
2187
+ this.currentSessionId = undefined;
2188
+ this.lastPlaybackDuration = 0;
2189
+ this.lastPlaybackStartTimestamp = 0;
2190
+ this.lastStallCount = 0;
2191
+ this.lastStallDuration = 0;
2192
+ this.loadStartTimestamp = 0;
2193
+ this.metadataRequestTime = 0;
2194
+ this.mediaAssetUrl = undefined;
2195
+ this.mediaId = undefined;
2196
+ this.mediaMetadataUrl = undefined;
2197
+ this.mediaOrigin = undefined;
2198
+ this.sessionStartTimestamp = undefined;
2199
+ this.tokenRequestTime = 0;
2200
+ clearInterval(this.heartbeatIntervalId);
2201
+ if (event && !['ended', 'playerreset'].includes(event.type)) {
2202
+ this.removeListeners();
2203
+ }
2204
+ }
2205
+
2206
+ /**
2207
+ * Sends an event to the server using the Beacon API.
2208
+ *
2209
+ * @param {string} eventName Either START, STOP, ERROR, HEARTBEAT
2210
+ * @param {Object} [data={}] The payload object to be sent. Defaults to an
2211
+ * empty object if not provided
2212
+ */
2213
+ sendEvent(eventName, data = {}) {
2214
+ // If the tracker is disabled for the current session, and there has been no
2215
+ // previous session, no event is sent. However, if a session was already
2216
+ // active, we still want to send the STOP event so that it is properly
2217
+ // stopped.
2218
+ if (this.isTrackerDisabled() && !this.currentSessionId || !this.currentSessionId) return;
2219
+ const payload = JSON.stringify({
2220
+ event_name: eventName,
2221
+ session_id: this.currentSessionId,
2222
+ timestamp: PillarboxMonitoring.timestamp(),
2223
+ version: this.schemaVersion,
2224
+ data
2225
+ });
2226
+ navigator.sendBeacon(this.beaconUrl, payload);
2227
+ }
2228
+
2229
+ /**
2230
+ * Starts a new session by first stopping the previous session, then resetting
2231
+ * the session start timestamp and media ID to their new values.
2232
+ */
2233
+ sessionStart() {
2234
+ if (this.sessionStartTimestamp) {
2235
+ this.sessionStop();
2236
+ }
2237
+
2238
+ // Reference timestamp used to calculate the different time metrics.
2239
+ this.sessionStartTimestamp = PillarboxMonitoring.timestamp();
2240
+ // At this stage currentSource().src is the media identifier
2241
+ // and not the playable source.
2242
+ this.mediaId = this.player.currentSource().src || undefined;
2243
+ }
2244
+
2245
+ /**
2246
+ * Stops the current session by sending a `STOP` event and resetting the
2247
+ * session.
2248
+ *
2249
+ * @param {Event} [event] The event that triggered the stop. This is passed
2250
+ * to the reset function.
2251
+ */
2252
+ sessionStop(event) {
2253
+ this.sendEvent('STOP', this.statusEventData());
2254
+ this.reset(event);
2255
+ }
2256
+
2257
+ /**
2258
+ * Handles the stalled state of the player. Sets the stalled state and listens
2259
+ * for the event that indicates the player is no longer stalled.
2260
+ */
2261
+ stalled() {
2262
+ if (!this.player.hasStarted() || this.player.seeking() || this.isStalled) return;
2263
+ this.isStalled = true;
2264
+ const stallStart = PillarboxMonitoring.timestamp();
2265
+ const unstalled = () => {
2266
+ const stallEnd = PillarboxMonitoring.timestamp();
2267
+ this.isStalled = false;
2268
+ this.lastStallCount += 1;
2269
+ this.lastStallDuration += stallEnd - stallStart;
2270
+ };
2271
+
2272
+ // As Safari is not consistent with its playing event, it is better to use
2273
+ // the timeupdate event.
2274
+ if (pillarbox.browser.IS_ANY_SAFARI) {
2275
+ this.player.one('timeupdate', unstalled);
2276
+ } else {
2277
+ // As Chromium-based browsers are not consistent with their timeupdate
2278
+ // event, it is better to use the playing event.
2279
+ //
2280
+ // Firefox is consistent with its playing event.
2281
+ this.player.one('playing', unstalled);
2282
+ }
2283
+ }
2284
+
2285
+ /**
2286
+ * Information about the player's stall events.
2287
+ *
2288
+ * @typedef {Object} StallInfo
2289
+ * @property {number} count The number of stall events
2290
+ * @property {number} duration The total duration of stall events in
2291
+ * milliseconds
2292
+ *
2293
+ * @returns {StallInfo} An object containing the stall information
2294
+ */
2295
+ stallInfo() {
2296
+ return {
2297
+ count: this.lastStallCount,
2298
+ duration: this.lastStallDuration
2299
+ };
2300
+ }
2301
+
2302
+ /**
2303
+ * Get data on the current playback state. Will be used when sending `HEARTBEAT` or `STOP` events.
2304
+ *
2305
+ * @typedef {Object} StatusEventData
2306
+ * @property {number} bandwidth The current bandwidth
2307
+ * @property {number|undefined} bitrate The bitrate of the current resource
2308
+ * @property {number} buffered_duration The duration of the buffered content
2309
+ * @property {number} frame_drops The number of dropped frames
2310
+ * @property {number} playback_duration The duration of the playback
2311
+ * @property {number} position The current playback position
2312
+ * @property {number} position_timestamp The timestamp of the current playback position
2313
+ * @property {Object} stall Information about any stalls
2314
+ * @property {string} stream_type The type of stream, either 'on-demand' or 'live'
2315
+ * @property {string|undefined} url The URL of the current resource
2316
+ *
2317
+ * @returns {StatusEventData} The current event data
2318
+ */
2319
+ statusEventData() {
2320
+ const bandwidth = this.bandwidth();
2321
+ const buffered_duration = this.bufferDuration();
2322
+ const {
2323
+ bitrate,
2324
+ url
2325
+ } = this.currentResource();
2326
+ const {
2327
+ droppedVideoFrames: frame_drops
2328
+ } = this.player.getVideoPlaybackQuality();
2329
+ const playback_duration = this.playbackDuration();
2330
+ const {
2331
+ position,
2332
+ position_timestamp
2333
+ } = this.playbackPosition();
2334
+ const stream_type = isFinite(this.player.duration()) ? 'On-demand' : 'Live';
2335
+ const stall = this.stallInfo();
2336
+ const data = {
2337
+ bandwidth,
2338
+ bitrate,
2339
+ buffered_duration,
2340
+ frame_drops,
2341
+ playback_duration,
2342
+ position,
2343
+ position_timestamp,
2344
+ stall,
2345
+ stream_type,
2346
+ url
2347
+ };
2348
+ return data;
2349
+ }
2350
+
2351
+ /**
2352
+ * Generates the data for the start event.
2353
+ *
2354
+ * @typedef {Object} Device
2355
+ * @property {string} id The device ID.
2356
+ *
2357
+ * @typedef {Object} StartEventData
2358
+ * @property {string} browser The user agent string of the browser.
2359
+ * @property {Device} device Information about the device.
2360
+ * @property {MediaInfo} media Information about the media.
2361
+ * @property {PlayerInfo} player Information about the player.
2362
+ * @property {QoeTimings} qoe_timings Quality of Experience timings.
2363
+ * @property {QosTimings} qos_timings Quality of Service timings.
2364
+ * @property {PlayerCurrentDimensions} screen The current dimensions of the
2365
+ * player.
2366
+ *
2367
+ * @returns {StartEventData} An object containing the start event data.
2368
+ */
2369
+ startEventData() {
2370
+ const timestamp = PillarboxMonitoring.timestamp();
2371
+ // This avoids false subtraction results when loadStartTimestamp is not
2372
+ // initialized.
2373
+ // loadStartTimestamp will be 0 if loadstart is not triggered.
2374
+ // This is the case when a STARTDATE error occurs.
2375
+ const timeToLoadedData = this.loadStartTimestamp ? timestamp - this.loadStartTimestamp : 0;
2376
+ if (!this.isTrackerDisabled()) {
2377
+ this.currentSessionId = PillarboxMonitoring.sessionId();
2378
+ }
2379
+ this.mediaAssetUrl = this.removeTokenFromAssetUrl(this.player.currentSource().src);
2380
+ this.mediaMetadataUrl = this.getMetadataInfo(this.mediaId).name;
2381
+ this.metadataRequestTime = this.getMetadataInfo(this.mediaId).duration;
2382
+ this.mediaOrigin = window.location.href;
2383
+ this.tokenRequestTime = this.getTokenRequestDuration(this.currentSourceMediaData().tokenType);
2384
+ return {
2385
+ browser: PillarboxMonitoring.userAgent(),
2386
+ device: {
2387
+ id: PillarboxMonitoring.deviceId()
2388
+ },
2389
+ media: this.mediaInfo(),
2390
+ player: this.playerInfo(),
2391
+ qoe_timings: this.qoeTimings(timeToLoadedData, timestamp),
2392
+ qos_timings: this.qosTimings(timeToLoadedData),
2393
+ screen: this.playerCurrentDimensions()
2394
+ };
2395
+ }
2396
+
2397
+ /**
2398
+ * Generates a new session ID.
2399
+ *
2400
+ * @returns {string} random UUID
2401
+ */
2402
+ static sessionId() {
2403
+ return PillarboxMonitoring.randomUUID();
2404
+ }
2405
+
2406
+ /**
2407
+ * Retrieve or generate a unique device ID and stores it in localStorage.
2408
+ *
2409
+ * @returns {string|undefined} The device ID if localStorage is available,
2410
+ * otherwise `undefined`
2411
+ */
2412
+ static deviceId() {
2413
+ if (!localStorage) return;
2414
+ const deviceIdKey = 'pillarbox_device_id';
2415
+ let deviceId = localStorage.getItem(deviceIdKey);
2416
+ if (!deviceId) {
2417
+ deviceId = PillarboxMonitoring.randomUUID();
2418
+ localStorage.setItem(deviceIdKey, deviceId);
2419
+ }
2420
+ return deviceId;
2421
+ }
2422
+
2423
+ /**
2424
+ * Generate a cryptographically secure random UUID.
2425
+ *
2426
+ * @returns {string}
2427
+ */
2428
+ static randomUUID() {
2429
+ if (!crypto.randomUUID) {
2430
+ // Polyfill from the author of uuid js which is simple and
2431
+ // cryptographically secure.
2432
+ // https://stackoverflow.com/a/2117523
2433
+ return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, c =>
2434
+ // eslint-disable-next-line
2435
+ (+c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> +c / 4).toString(16));
2436
+ }
2437
+ return crypto.randomUUID();
2438
+ }
2439
+
2440
+ /**
2441
+ * converts seconds into milliseconds.
2442
+ *
2443
+ * @param {number} seconds
2444
+ *
2445
+ * @returns {number} milliseconds as an integer value
2446
+ */
2447
+ static secondsToMilliseconds(seconds) {
2448
+ return parseInt(seconds * 1000);
2449
+ }
2450
+
2451
+ /**
2452
+ * The timestamp in milliseconds.
2453
+ *
2454
+ * @return {number} milliseconds as an integer value
2455
+ */
2456
+ static timestamp() {
2457
+ return Date.now();
2458
+ }
2459
+
2460
+ /**
2461
+ * The browser's user agent.
2462
+ *
2463
+ * @returns {string}
2464
+ */
2465
+ static userAgent() {
2466
+ return {
2467
+ user_agent: navigator.userAgent
2468
+ };
2469
+ }
2470
+ }
2471
+
1646
2472
  /**
1647
2473
  * Represents the composition of media content.
1648
2474
  *
@@ -3407,6 +4233,9 @@ class SrgSsr {
3407
4233
  */
3408
4234
  static getSrcMediaObj(player, srcObj) {
3409
4235
  return _asyncToGenerator(function* () {
4236
+ if (SrgSsr.pillarboxMonitoring(player)) {
4237
+ SrgSsr.pillarboxMonitoring(player).sessionStart();
4238
+ }
3410
4239
  const {
3411
4240
  src: urn
3412
4241
  } = srcObj,
@@ -3524,6 +4353,30 @@ class SrgSsr {
3524
4353
  }
3525
4354
  }
3526
4355
 
4356
+ /**
4357
+ * PillarboxMonitoring monitoring singleton.
4358
+ *
4359
+ * @param {import('video.js/dist/types/player').default} player
4360
+ *
4361
+ * @returns {PillarboxMonitoring} instance of PillarboxMonitoring
4362
+ */
4363
+ static pillarboxMonitoring(player) {
4364
+ if (player.options().trackers.pillarboxMonitoring === false) return;
4365
+ if (!player.options().trackers.pillarboxMonitoring) {
4366
+ const pillarboxMonitoring = new PillarboxMonitoring(player, {
4367
+ debug: player.debug(),
4368
+ playerVersion: pillarbox.VERSION.pillarbox,
4369
+ playerName: 'Pillarbox'
4370
+ });
4371
+ player.options({
4372
+ trackers: {
4373
+ pillarboxMonitoring
4374
+ }
4375
+ });
4376
+ }
4377
+ return player.options().trackers.pillarboxMonitoring;
4378
+ }
4379
+
3527
4380
  /**
3528
4381
  * Update player's poster.
3529
4382
  *
@@ -3559,6 +4412,7 @@ class SrgSsr {
3559
4412
  * @returns {Object}
3560
4413
  */
3561
4414
  static middleware(player) {
4415
+ SrgSsr.pillarboxMonitoring(player);
3562
4416
  SrgSsr.cuechangeEventProxy(player);
3563
4417
  return {
3564
4418
  currentTime: currentTime => SrgSsr.handleCurrentTime(player, currentTime),