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