@srgssr/pillarbox-web 1.13.0 → 1.14.0

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.12.2";
113
+ var version = "1.13.1";
114
114
 
115
115
  /**
116
116
  * @ignore
@@ -1643,6 +1643,833 @@ 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
+ severity: 'Fatal',
1888
+ url
1889
+ }));
1890
+ this.reset();
1891
+ }
1892
+
1893
+ /**
1894
+ * Get the DRM license request duration from performance API.
1895
+ *
1896
+ * @returns {number|undefined} The request duration
1897
+ */
1898
+ getDrmRequestDuration() {
1899
+ const keySystems = Object.values(this.player.currentSource().keySystems || {}).map(keySystem => keySystem.url);
1900
+ if (!keySystems.length) return;
1901
+ const resource = performance.getEntriesByType('resource').filter(({
1902
+ initiatorType,
1903
+ name
1904
+ }) => initiatorType === 'xmlhttprequest' && keySystems.includes(name)).pop();
1905
+ return resource && resource.duration;
1906
+ }
1907
+
1908
+ /**
1909
+ * Get metadata information from the performance API for a given id.
1910
+ *
1911
+ * @typedef {Object} MetadataInfo
1912
+ * @property {string} name The URL of the resource
1913
+ * @property {number} duration The duration of the resource fetch in milliseconds
1914
+ *
1915
+ * @param {string} id The id to search for in the resource entries
1916
+ *
1917
+ * @returns {MetadataInfo|undefined} An object containing metadata
1918
+ * information, or undefined otherwise
1919
+ */
1920
+ getMetadataInfo(id) {
1921
+ const resource = performance.getEntriesByType('resource').filter(({
1922
+ initiatorType,
1923
+ name
1924
+ }) => initiatorType === 'fetch' && name.includes(id)).pop();
1925
+ if (!resource) return {};
1926
+ return {
1927
+ name: resource.name,
1928
+ duration: resource.duration
1929
+ };
1930
+ }
1931
+
1932
+ /**
1933
+ * Get the Akamai token request duration from performance API.
1934
+ *
1935
+ * @returns {number|undefined} The request duration
1936
+ */
1937
+ getTokenRequestDuration(tokenType) {
1938
+ if (!tokenType) return;
1939
+ const resource = performance.getEntriesByType('resource').filter(({
1940
+ initiatorType,
1941
+ name
1942
+ }) => initiatorType === 'fetch' && name.includes('/akahd/token')).pop();
1943
+ return resource && resource.duration;
1944
+ }
1945
+
1946
+ /**
1947
+ * Send an 'HEARTBEAT' event with the date of the current playback state at
1948
+ * regular intervals.
1949
+ */
1950
+ heartbeat() {
1951
+ this.heartbeatIntervalId = setInterval(() => {
1952
+ this.sendEvent('HEARTBEAT', this.statusEventData());
1953
+ }, this.heartbeatInterval);
1954
+ }
1955
+
1956
+ /**
1957
+ * Check if the tracker is disabled.
1958
+ *
1959
+ * @returns {Boolean} __true__ if disabled __false__ otherwise.
1960
+ */
1961
+ isTrackerDisabled() {
1962
+ const currentSource = this.player.currentSource();
1963
+ if (!Array.isArray(currentSource.disableTrackers)) {
1964
+ return Boolean(currentSource.disableTrackers);
1965
+ }
1966
+ return Boolean(currentSource.disableTrackers.find(tracker => tracker.toLowerCase() === PillarboxMonitoring.name.toLowerCase()));
1967
+ }
1968
+
1969
+ /**
1970
+ * Handles the session start by sending a `START` event immediately followed
1971
+ * by a `HEARTBEAT` when the `loadeddata` event is triggered.
1972
+ */
1973
+ loadedData() {
1974
+ this.sendEvent('START', this.startEventData());
1975
+ this.sendEvent('HEARTBEAT', this.statusEventData());
1976
+ // starts the heartbeat interval
1977
+ this.heartbeat();
1978
+ }
1979
+
1980
+ /**
1981
+ * Handles `loadstart` event and captures the current timestamp. Will be used
1982
+ * to calculate the media loading time.
1983
+ */
1984
+ loadStart() {
1985
+ // if the content is a plain old URL
1986
+ if (!Object.keys(this.currentSourceMediaData()).length && this.currentSessionId) {
1987
+ this.sessionStop();
1988
+ // Reference timestamp used to calculate the different time metrics.
1989
+ this.sessionStartTimestamp = PillarboxMonitoring.timestamp();
1990
+ }
1991
+ this.loadStartTimestamp = PillarboxMonitoring.timestamp();
1992
+ }
1993
+
1994
+ /**
1995
+ * The media information.
1996
+ *
1997
+ * @typedef {Object} MediaInfo
1998
+ * @property {string} asset_url The URL of the media
1999
+ * @property {string} id The ID of the media
2000
+ * @property {string} metadata_url The URL of the media metadata
2001
+ * @property {string} origin The origin of the media
2002
+ *
2003
+ * @returns {MediaInfo} An object container the media information
2004
+ */
2005
+ mediaInfo() {
2006
+ return {
2007
+ asset_url: this.mediaAssetUrl,
2008
+ id: this.mediaId,
2009
+ metadata_url: this.mediaMetadataUrl,
2010
+ origin: this.mediaOrigin
2011
+ };
2012
+ }
2013
+
2014
+ /**
2015
+ * The total playback duration for the current session.
2016
+ *
2017
+ * @returns {number} The total playback duration in milliseconds.
2018
+ */
2019
+ playbackDuration() {
2020
+ if (!this.lastPlaybackStartTimestamp) {
2021
+ return this.lastPlaybackDuration;
2022
+ }
2023
+ return PillarboxMonitoring.timestamp() + this.lastPlaybackDuration - this.lastPlaybackStartTimestamp;
2024
+ }
2025
+
2026
+ /**
2027
+ * The current playback position and position timestamp.
2028
+ *
2029
+ * @typedef {Object} PlaybackPosition
2030
+ * @property {number} position The current playback position in milliseconds
2031
+ * @property {number|undefined} position_timestamp The timestamp of the
2032
+ * current playback position, or undefined if not available
2033
+ *
2034
+ * @returns {PlaybackPosition} The playback position object.
2035
+ */
2036
+ playbackPosition() {
2037
+ const currentRepresentation = this.currentRepresentation();
2038
+ const position = PillarboxMonitoring.secondsToMilliseconds(this.player.currentTime());
2039
+ let position_timestamp;
2040
+
2041
+ // Get the position timestamp from the program date time when VHS is used
2042
+ // or undefined if there is no value
2043
+ if (currentRepresentation) {
2044
+ position_timestamp = currentRepresentation.programDateTime;
2045
+ }
2046
+
2047
+ // Calculate the position timestamp from the start date on Safari
2048
+ if (pillarbox.browser.IS_ANY_SAFARI) {
2049
+ const startDate = Date.parse(this.player.$('video').getStartDate());
2050
+ position_timestamp = !isNaN(startDate) ? startDate + position : undefined;
2051
+ }
2052
+ return {
2053
+ position,
2054
+ position_timestamp
2055
+ };
2056
+ }
2057
+
2058
+ /**
2059
+ * Assign the timestamp each time the playback starts.
2060
+ */
2061
+ playbackStart() {
2062
+ this.lastPlaybackStartTimestamp = PillarboxMonitoring.timestamp();
2063
+ }
2064
+
2065
+ /**
2066
+ * Calculates and accumulates the duration of the playback session each time
2067
+ * the playback stops for the current media.
2068
+ */
2069
+ playbackStop() {
2070
+ this.lastPlaybackDuration += PillarboxMonitoring.timestamp() - this.lastPlaybackStartTimestamp;
2071
+ this.lastPlaybackStartTimestamp = 0;
2072
+ }
2073
+
2074
+ /**
2075
+ * The current dimensions of the player.
2076
+ *
2077
+ * @typedef {Object} PlayerCurrentDimensions
2078
+ * @property {number} width The current width of the player
2079
+ * @property {number} height The current height of the player
2080
+ *
2081
+ * @returns {PlayerCurrentDimensions} The current dimensions of the player object.
2082
+ */
2083
+ playerCurrentDimensions() {
2084
+ return this.player.currentDimensions();
2085
+ }
2086
+
2087
+ /**
2088
+ * Information about the player.
2089
+ *
2090
+ * @typedef {Object} PlayerInfo
2091
+ * @property {string} name The name of the player
2092
+ * @property {string} version The version of the player
2093
+ * @property {string} platform The platform on which the player is running
2094
+ *
2095
+ * @returns {PlayerInfo} An object containing player information.
2096
+ */
2097
+ playerInfo() {
2098
+ return {
2099
+ name: this.playerName,
2100
+ version: this.playerVersion,
2101
+ platform: this.platform
2102
+ };
2103
+ }
2104
+
2105
+ /**
2106
+ * Generates the QoE timings object.
2107
+ *
2108
+ * @typedef {Object} QoeTimings
2109
+ * @property {number} metadata The time taken to load metadata
2110
+ * @property {number} asset The time taken to load the asset
2111
+ * @property {number} total The total time taken from session start to data load
2112
+ *
2113
+ * @param {number} timeToLoadedData The time taken to load the data
2114
+ * @param {number} timestamp The current timestamp
2115
+ *
2116
+ * @returns {QoeTimings} The QoE timings
2117
+ */
2118
+ qoeTimings(timeToLoadedData, timestamp) {
2119
+ return {
2120
+ metadata: this.metadataRequestTime,
2121
+ asset: timeToLoadedData,
2122
+ total: timestamp - this.sessionStartTimestamp
2123
+ };
2124
+ }
2125
+
2126
+ /**
2127
+ * Generates the QoS timings object.
2128
+ *
2129
+ * @typedef {Object} QosTimings
2130
+ * @property {number} asset The time taken to load the asset
2131
+ * @property {number} drm The time taken for DRM processing
2132
+ * @property {number} metadata The time taken to load metadata
2133
+ * @property {number} token The time taken to request the token
2134
+ *
2135
+ * @param {number} timeToLoadedData The time taken to load the data
2136
+ *
2137
+ * @returns {QosTimings} The QoS timings
2138
+ */
2139
+ qosTimings(timeToLoadedData) {
2140
+ return {
2141
+ asset: timeToLoadedData,
2142
+ drm: this.getDrmRequestDuration(),
2143
+ metadata: this.metadataRequestTime,
2144
+ token: this.tokenRequestTime
2145
+ };
2146
+ }
2147
+
2148
+ /**
2149
+ * Removes all event listeners from the player and the window.
2150
+ */
2151
+ removeListeners() {
2152
+ this.player.off('loadstart', this.loadStart);
2153
+ this.player.off('loadeddata', this.loadedData);
2154
+ this.player.off('playing', this.playbackStart);
2155
+ this.player.off('pause', this.playbackStop);
2156
+ this.player.off('error', this.error);
2157
+ this.player.off(['playerreset', 'dispose', 'ended'], this.sessionStop);
2158
+ this.player.off(['waiting', 'stalled'], this.stalled);
2159
+ window.removeEventListener('beforeunload', this.sessionStop);
2160
+ }
2161
+
2162
+ /**
2163
+ * Remove the token from the asset URL.
2164
+ *
2165
+ * @param {string} assetUrl The URL of the asset
2166
+ *
2167
+ * @returns {string|undefined} The URL without the token, or undefined if the
2168
+ * input URL is invalid
2169
+ */
2170
+ removeTokenFromAssetUrl(assetUrl) {
2171
+ if (!assetUrl) return;
2172
+ try {
2173
+ const url = new URL(assetUrl);
2174
+ url.searchParams.delete('hdnts');
2175
+ return url.href;
2176
+ } catch (e) {
2177
+ return;
2178
+ }
2179
+ }
2180
+
2181
+ /**
2182
+ * Resets the playback session and clears relevant properties.
2183
+ *
2184
+ * @param {Event} event The event that triggered the reset. If the event type
2185
+ * is not 'ended' or 'playerreset', listeners will be removed.
2186
+ */
2187
+ reset(event) {
2188
+ this.currentSessionId = undefined;
2189
+ this.lastPlaybackDuration = 0;
2190
+ this.lastPlaybackStartTimestamp = 0;
2191
+ this.lastStallCount = 0;
2192
+ this.lastStallDuration = 0;
2193
+ this.loadStartTimestamp = 0;
2194
+ this.metadataRequestTime = 0;
2195
+ this.mediaAssetUrl = undefined;
2196
+ this.mediaId = undefined;
2197
+ this.mediaMetadataUrl = undefined;
2198
+ this.mediaOrigin = undefined;
2199
+ this.sessionStartTimestamp = undefined;
2200
+ this.tokenRequestTime = 0;
2201
+ clearInterval(this.heartbeatIntervalId);
2202
+ if (event && !['ended', 'playerreset'].includes(event.type)) {
2203
+ this.removeListeners();
2204
+ }
2205
+ }
2206
+
2207
+ /**
2208
+ * Sends an event to the server using the Beacon API.
2209
+ *
2210
+ * @param {string} eventName Either START, STOP, ERROR, HEARTBEAT
2211
+ * @param {Object} [data={}] The payload object to be sent. Defaults to an
2212
+ * empty object if not provided
2213
+ */
2214
+ sendEvent(eventName, data = {}) {
2215
+ // If the tracker is disabled for the current session, and there has been no
2216
+ // previous session, no event is sent. However, if a session was already
2217
+ // active, we still want to send the STOP event so that it is properly
2218
+ // stopped.
2219
+ if (this.isTrackerDisabled() && !this.currentSessionId || !this.currentSessionId) return;
2220
+ const payload = JSON.stringify({
2221
+ event_name: eventName,
2222
+ session_id: this.currentSessionId,
2223
+ timestamp: PillarboxMonitoring.timestamp(),
2224
+ version: this.schemaVersion,
2225
+ data
2226
+ });
2227
+ navigator.sendBeacon(this.beaconUrl, payload);
2228
+ }
2229
+
2230
+ /**
2231
+ * Starts a new session by first stopping the previous session, then resetting
2232
+ * the session start timestamp and media ID to their new values.
2233
+ */
2234
+ sessionStart() {
2235
+ if (this.sessionStartTimestamp) {
2236
+ this.sessionStop();
2237
+ }
2238
+
2239
+ // Reference timestamp used to calculate the different time metrics.
2240
+ this.sessionStartTimestamp = PillarboxMonitoring.timestamp();
2241
+ // At this stage currentSource().src is the media identifier
2242
+ // and not the playable source.
2243
+ this.mediaId = this.player.currentSource().src || undefined;
2244
+ }
2245
+
2246
+ /**
2247
+ * Stops the current session by sending a `STOP` event and resetting the
2248
+ * session.
2249
+ *
2250
+ * @param {Event} [event] The event that triggered the stop. This is passed
2251
+ * to the reset function.
2252
+ */
2253
+ sessionStop(event) {
2254
+ this.sendEvent('STOP', this.statusEventData());
2255
+ this.reset(event);
2256
+ }
2257
+
2258
+ /**
2259
+ * Handles the stalled state of the player. Sets the stalled state and listens
2260
+ * for the event that indicates the player is no longer stalled.
2261
+ */
2262
+ stalled() {
2263
+ if (!this.player.hasStarted() || this.player.seeking() || this.isStalled) return;
2264
+ this.isStalled = true;
2265
+ const stallStart = PillarboxMonitoring.timestamp();
2266
+ const unstalled = () => {
2267
+ const stallEnd = PillarboxMonitoring.timestamp();
2268
+ this.isStalled = false;
2269
+ this.lastStallCount += 1;
2270
+ this.lastStallDuration += stallEnd - stallStart;
2271
+ };
2272
+
2273
+ // As Safari is not consistent with its playing event, it is better to use
2274
+ // the timeupdate event.
2275
+ if (pillarbox.browser.IS_ANY_SAFARI) {
2276
+ this.player.one('timeupdate', unstalled);
2277
+ } else {
2278
+ // As Chromium-based browsers are not consistent with their timeupdate
2279
+ // event, it is better to use the playing event.
2280
+ //
2281
+ // Firefox is consistent with its playing event.
2282
+ this.player.one('playing', unstalled);
2283
+ }
2284
+ }
2285
+
2286
+ /**
2287
+ * Information about the player's stall events.
2288
+ *
2289
+ * @typedef {Object} StallInfo
2290
+ * @property {number} count The number of stall events
2291
+ * @property {number} duration The total duration of stall events in
2292
+ * milliseconds
2293
+ *
2294
+ * @returns {StallInfo} An object containing the stall information
2295
+ */
2296
+ stallInfo() {
2297
+ return {
2298
+ count: this.lastStallCount,
2299
+ duration: this.lastStallDuration
2300
+ };
2301
+ }
2302
+
2303
+ /**
2304
+ * Get data on the current playback state. Will be used when sending `HEARTBEAT` or `STOP` events.
2305
+ *
2306
+ * @typedef {Object} StatusEventData
2307
+ * @property {number} bandwidth The current bandwidth
2308
+ * @property {number|undefined} bitrate The bitrate of the current resource
2309
+ * @property {number} buffered_duration The duration of the buffered content
2310
+ * @property {number} frame_drops The number of dropped frames
2311
+ * @property {number} playback_duration The duration of the playback
2312
+ * @property {number} position The current playback position
2313
+ * @property {number} position_timestamp The timestamp of the current playback position
2314
+ * @property {Object} stall Information about any stalls
2315
+ * @property {string} stream_type The type of stream, either 'on-demand' or 'live'
2316
+ * @property {string|undefined} url The URL of the current resource
2317
+ *
2318
+ * @returns {StatusEventData} The current event data
2319
+ */
2320
+ statusEventData() {
2321
+ const bandwidth = this.bandwidth();
2322
+ const buffered_duration = this.bufferDuration();
2323
+ const {
2324
+ bitrate,
2325
+ url
2326
+ } = this.currentResource();
2327
+ const {
2328
+ droppedVideoFrames: frame_drops
2329
+ } = this.player.getVideoPlaybackQuality();
2330
+ const playback_duration = this.playbackDuration();
2331
+ const {
2332
+ position,
2333
+ position_timestamp
2334
+ } = this.playbackPosition();
2335
+ const stream_type = isFinite(this.player.duration()) ? 'On-demand' : 'Live';
2336
+ const stall = this.stallInfo();
2337
+ const data = {
2338
+ bandwidth,
2339
+ bitrate,
2340
+ buffered_duration,
2341
+ frame_drops,
2342
+ playback_duration,
2343
+ position,
2344
+ position_timestamp,
2345
+ stall,
2346
+ stream_type,
2347
+ url
2348
+ };
2349
+ return data;
2350
+ }
2351
+
2352
+ /**
2353
+ * Generates the data for the start event.
2354
+ *
2355
+ * @typedef {Object} Device
2356
+ * @property {string} id The device ID.
2357
+ *
2358
+ * @typedef {Object} StartEventData
2359
+ * @property {string} browser The user agent string of the browser.
2360
+ * @property {Device} device Information about the device.
2361
+ * @property {MediaInfo} media Information about the media.
2362
+ * @property {PlayerInfo} player Information about the player.
2363
+ * @property {QoeTimings} qoe_timings Quality of Experience timings.
2364
+ * @property {QosTimings} qos_timings Quality of Service timings.
2365
+ * @property {PlayerCurrentDimensions} screen The current dimensions of the
2366
+ * player.
2367
+ *
2368
+ * @returns {StartEventData} An object containing the start event data.
2369
+ */
2370
+ startEventData() {
2371
+ const timestamp = PillarboxMonitoring.timestamp();
2372
+ // This avoids false subtraction results when loadStartTimestamp is not
2373
+ // initialized.
2374
+ // loadStartTimestamp will be 0 if loadstart is not triggered.
2375
+ // This is the case when a STARTDATE error occurs.
2376
+ const timeToLoadedData = this.loadStartTimestamp ? timestamp - this.loadStartTimestamp : 0;
2377
+ if (!this.isTrackerDisabled()) {
2378
+ this.currentSessionId = PillarboxMonitoring.sessionId();
2379
+ }
2380
+ this.mediaAssetUrl = this.removeTokenFromAssetUrl(this.player.currentSource().src);
2381
+ this.mediaMetadataUrl = this.getMetadataInfo(this.mediaId).name;
2382
+ this.metadataRequestTime = this.getMetadataInfo(this.mediaId).duration;
2383
+ this.mediaOrigin = window.location.href;
2384
+ this.tokenRequestTime = this.getTokenRequestDuration(this.currentSourceMediaData().tokenType);
2385
+ return {
2386
+ browser: PillarboxMonitoring.userAgent(),
2387
+ device: {
2388
+ id: PillarboxMonitoring.deviceId()
2389
+ },
2390
+ media: this.mediaInfo(),
2391
+ player: this.playerInfo(),
2392
+ qoe_timings: this.qoeTimings(timeToLoadedData, timestamp),
2393
+ qos_timings: this.qosTimings(timeToLoadedData),
2394
+ screen: this.playerCurrentDimensions()
2395
+ };
2396
+ }
2397
+
2398
+ /**
2399
+ * Generates a new session ID.
2400
+ *
2401
+ * @returns {string} random UUID
2402
+ */
2403
+ static sessionId() {
2404
+ return PillarboxMonitoring.randomUUID();
2405
+ }
2406
+
2407
+ /**
2408
+ * Retrieve or generate a unique device ID and stores it in localStorage.
2409
+ *
2410
+ * @returns {string|undefined} The device ID if localStorage is available,
2411
+ * otherwise `undefined`
2412
+ */
2413
+ static deviceId() {
2414
+ if (!localStorage) return;
2415
+ const deviceIdKey = 'pillarbox_device_id';
2416
+ let deviceId = localStorage.getItem(deviceIdKey);
2417
+ if (!deviceId) {
2418
+ deviceId = PillarboxMonitoring.randomUUID();
2419
+ localStorage.setItem(deviceIdKey, deviceId);
2420
+ }
2421
+ return deviceId;
2422
+ }
2423
+
2424
+ /**
2425
+ * Generate a cryptographically secure random UUID.
2426
+ *
2427
+ * @returns {string}
2428
+ */
2429
+ static randomUUID() {
2430
+ if (!crypto.randomUUID) {
2431
+ // Polyfill from the author of uuid js which is simple and
2432
+ // cryptographically secure.
2433
+ // https://stackoverflow.com/a/2117523
2434
+ return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, c =>
2435
+ // eslint-disable-next-line
2436
+ (+c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> +c / 4).toString(16));
2437
+ }
2438
+ return crypto.randomUUID();
2439
+ }
2440
+
2441
+ /**
2442
+ * converts seconds into milliseconds.
2443
+ *
2444
+ * @param {number} seconds
2445
+ *
2446
+ * @returns {number} milliseconds as an integer value
2447
+ */
2448
+ static secondsToMilliseconds(seconds) {
2449
+ return parseInt(seconds * 1000);
2450
+ }
2451
+
2452
+ /**
2453
+ * The timestamp in milliseconds.
2454
+ *
2455
+ * @return {number} milliseconds as an integer value
2456
+ */
2457
+ static timestamp() {
2458
+ return Date.now();
2459
+ }
2460
+
2461
+ /**
2462
+ * The browser's user agent.
2463
+ *
2464
+ * @returns {string}
2465
+ */
2466
+ static userAgent() {
2467
+ return {
2468
+ user_agent: navigator.userAgent
2469
+ };
2470
+ }
2471
+ }
2472
+
1646
2473
  /**
1647
2474
  * Represents the composition of media content.
1648
2475
  *
@@ -3164,8 +3991,9 @@ class SrgSsr {
3164
3991
  * @returns {Array.<import('./typedef').MainResourceWithKeySystems>}
3165
3992
  */
3166
3993
  static composeKeySystemsResources(resources = []) {
3167
- if (!Drm.hasDrm(resources)) ;
3168
- return resources.map(resource => _objectSpread2(_objectSpread2({}, resource), Drm.buildKeySystems(resource.drmList)));
3994
+ if (!Drm.hasDrm(resources)) return resources;
3995
+ const resourcesWithKeySystems = resources.map(resource => _objectSpread2(_objectSpread2({}, resource), Drm.buildKeySystems(resource.drmList)));
3996
+ return resourcesWithKeySystems;
3169
3997
  }
3170
3998
 
3171
3999
  /**
@@ -3406,6 +4234,9 @@ class SrgSsr {
3406
4234
  */
3407
4235
  static getSrcMediaObj(player, srcObj) {
3408
4236
  return _asyncToGenerator(function* () {
4237
+ if (SrgSsr.pillarboxMonitoring(player)) {
4238
+ SrgSsr.pillarboxMonitoring(player).sessionStart();
4239
+ }
3409
4240
  const {
3410
4241
  src: urn
3411
4242
  } = srcObj,
@@ -3523,6 +4354,30 @@ class SrgSsr {
3523
4354
  }
3524
4355
  }
3525
4356
 
4357
+ /**
4358
+ * PillarboxMonitoring monitoring singleton.
4359
+ *
4360
+ * @param {import('video.js/dist/types/player').default} player
4361
+ *
4362
+ * @returns {PillarboxMonitoring} instance of PillarboxMonitoring
4363
+ */
4364
+ static pillarboxMonitoring(player) {
4365
+ if (player.options().trackers.pillarboxMonitoring === false) return;
4366
+ if (!player.options().trackers.pillarboxMonitoring) {
4367
+ const pillarboxMonitoring = new PillarboxMonitoring(player, {
4368
+ debug: player.debug(),
4369
+ playerVersion: pillarbox.VERSION.pillarbox,
4370
+ playerName: 'Pillarbox'
4371
+ });
4372
+ player.options({
4373
+ trackers: {
4374
+ pillarboxMonitoring
4375
+ }
4376
+ });
4377
+ }
4378
+ return player.options().trackers.pillarboxMonitoring;
4379
+ }
4380
+
3526
4381
  /**
3527
4382
  * Update player's poster.
3528
4383
  *
@@ -3558,6 +4413,7 @@ class SrgSsr {
3558
4413
  * @returns {Object}
3559
4414
  */
3560
4415
  static middleware(player) {
4416
+ SrgSsr.pillarboxMonitoring(player);
3561
4417
  SrgSsr.cuechangeEventProxy(player);
3562
4418
  return {
3563
4419
  currentTime: currentTime => SrgSsr.handleCurrentTime(player, currentTime),