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