@kkcompany/player 2.25.0-canary.24 → 2.25.0-canary.25

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.
package/dist/modules.mjs CHANGED
@@ -1,2219 +1,649 @@
1
- import mitt from 'mitt';
2
- import UAParser from 'ua-parser-js';
3
-
4
- /* eslint-disable no-param-reassign */
5
- const waitMs$1 = time => new Promise(resolve => {
6
- setTimeout(resolve, time);
1
+ import { D as subscribeCastState, P as dispatchChapterEvents, R as __exportAll, _ as loadMedia, c as uuidv4_default, g as linkCast, i as startSession_default, m as initSender, n as getContentInfo, o as logEventNames, r as getStreamInfo, s as mapLogEvents, t as createApi, u as disconnect } from "./api-2BOrEA5d.mjs";
2
+ import { r as getVersion } from "./util-B2YBSBjR.mjs";
3
+ import { A as once, C as validateEnvironment, S as needNativeHls, _ as getOS, g as getBrowser, i as isLiveDuration, k as on, y as isDesktop } from "./mediaBindings-CoY60lQw.mjs";
4
+ import { t as fixDashManifest_default } from "./fixDashManifest-CJ63KKaA.mjs";
5
+ import { t as selectHlsQualities } from "./adaptation-BcTsh-wx.mjs";
6
+ import mitt from "mitt";
7
+
8
+ //#region src/playerCore/playlogv3.js
9
+ var playlogv3_exports = /* @__PURE__ */ __exportAll({
10
+ logEventNames: () => logEventNames$1,
11
+ mapLogEvents: () => mapLogEvents$1
7
12
  });
8
-
9
- const handleRequestError = (request, {
10
- onError,
11
- retryTimes = 0
12
- }) => request().catch(error => onError(error, {
13
- retry: () => handleRequestError(request, {
14
- onError,
15
- retryTimes: retryTimes + 1
16
- }),
17
- retryTimes
18
- }));
19
-
20
- const ignoreMinorError = async (event, {
21
- retry,
22
- retryTimes
23
- } = {}) => {
24
- var _event$response, _event$response2, _event$response3;
25
-
26
- console.warn(event);
27
-
28
- if ((((_event$response = event.response) === null || _event$response === void 0 ? void 0 : _event$response.message) === 'Network Error' || /502|503/.test((_event$response2 = event.response) === null || _event$response2 === void 0 ? void 0 : _event$response2.status)) && retryTimes < 3) {
29
- await waitMs$1(3000);
30
- return retry();
31
- }
32
-
33
- const url = new URL(((_event$response3 = event.response) === null || _event$response3 === void 0 ? void 0 : _event$response3.url) || 'http://unknown/');
34
-
35
- if (/start$|info$|heartbeat$/.test(url.pathname)) {
36
- return Promise.reject(event);
37
- }
38
-
39
- if (/end$/.test(url.pathname)) {
40
- return Promise.resolve();
41
- }
42
-
43
- console.log('Ignore non-critical playback API fail', event);
44
- return new Promise(() => {});
45
- };
46
- /** @param {Response} response */
47
-
48
-
49
- const handleFetchResponse = response => {
50
- if (response.ok) {
51
- return response.status === 200 ? response.json() : '';
52
- }
53
-
54
- return response.json().then(errorData => {
55
- const error = new Error(`HTTP error status: ${response.status} ${errorData.message}`);
56
- error.data = errorData;
57
- error.response = response;
58
- return Promise.reject(error);
59
- });
60
- };
61
-
62
- const createApi = (config, {
63
- onError = ignoreMinorError
64
- } = {}) => {
65
- const {
66
- host,
67
- accessToken,
68
- deviceId,
69
- headers,
70
- params
71
- } = config;
72
-
73
- const getHeaders = () => ({ ...(accessToken && {
74
- Authorization: accessToken
75
- }),
76
- ...(deviceId && {
77
- 'X-Device-ID': deviceId
78
- }),
79
- 'Content-type': 'application/json',
80
- ...headers
81
- });
82
-
83
- const request = (url, {
84
- method
85
- } = {}) => handleRequestError(() => fetch(`${url}?${new URLSearchParams(params).toString()}`, {
86
- method,
87
- headers: getHeaders()
88
- }).then(handleFetchResponse), {
89
- onError
90
- });
91
-
92
- const sessionRequest = (path, {
93
- method = 'POST',
94
- type,
95
- id,
96
- token
97
- }) => handleRequestError(() => fetch(`${host}/sessions/${type}/${id}/playback/${deviceId}/${path}?${new URLSearchParams({ ...params,
98
- playback_token: token
99
- }).toString()}`, {
100
- method,
101
- headers: getHeaders()
102
- }).then(handleFetchResponse), {
103
- onError
104
- });
105
-
106
- return {
107
- config,
108
- getContent: ({
109
- type,
110
- id
111
- }) => request(`${host}/${type}/${id}`, {}),
112
- startPlayback: ({
113
- type,
114
- id
115
- }) => request(`${host}/sessions/${type}/${id}/playback/${deviceId}/start`, {
116
- method: 'POST'
117
- }),
118
- getPlaybackInfo: ({
119
- type,
120
- id,
121
- token
122
- }) => sessionRequest('info', {
123
- method: 'GET',
124
- type,
125
- id,
126
- token
127
- }),
128
- heartbeat: ({
129
- type,
130
- id,
131
- token
132
- }) => sessionRequest('heartbeat', {
133
- type,
134
- id,
135
- token
136
- }),
137
- updateLastPlayed: ({
138
- type,
139
- id,
140
- token,
141
- time
142
- }) => sessionRequest(`position/${Math.floor(time)}`, {
143
- type,
144
- id,
145
- token
146
- }),
147
- endPlayback: ({
148
- type,
149
- id,
150
- token
151
- }) => sessionRequest('end', {
152
- type,
153
- id,
154
- token
155
- })
156
- };
157
- };
158
-
159
- const getStreamInfo = (sources = [], {
160
- type = '',
161
- licenseUri,
162
- certificateUri = `${licenseUri}/fairplay_cert`,
163
- licenseHeaders: headers,
164
- thumbnailEnabled
165
- } = {}) => {
166
- const activeSource = sources.find(source => (source.subdub || source.type || '').toLowerCase() === type) || sources[0];
167
- return ((activeSource === null || activeSource === void 0 ? void 0 : activeSource.manifests) || []).map(manifest => ({ ...manifest,
168
- type: manifest.protocol,
169
- src: manifest.url,
170
- drm: {
171
- fairplay: {
172
- licenseUri,
173
- certificateUri,
174
- headers
175
- },
176
- widevine: {
177
- licenseUri,
178
- headers
179
- },
180
- playready: {
181
- licenseUri,
182
- headers
183
- }
184
- },
185
- qualityOptions: manifest.resolutions.map(({
186
- height
187
- }) => ({
188
- label: height,
189
- value: height,
190
- options: {
191
- maxHeight: height
192
- }
193
- }))
194
- })).concat(thumbnailEnabled && activeSource !== null && activeSource !== void 0 && activeSource.thumbnail_seeking_url ? {
195
- type: 'thumbnail',
196
- src: activeSource.thumbnail_seeking_url
197
- } : []);
198
- };
199
-
200
- const getOpeningSkipChapters = data => {
201
- const {
202
- opening_begin_time: opBegin,
203
- opening_end_time: opEnd
204
- } = data.time || {};
205
-
206
- if (opBegin !== null && opEnd !== null && opEnd - opBegin > 3) {
207
- const res = [{
208
- type: 'opening',
209
- startTime: opBegin + 0.2
210
- }, {
211
- name: 'part a',
212
- startTime: opEnd
213
- }];
214
- return res;
215
- }
216
-
217
- return [];
218
- };
219
-
220
- const getContentInfo = data => {
221
- var _data$channel_info, _data$time, _data$time2;
222
-
223
- return {
224
- title: data.title,
225
- channelTitle: data.subtitle,
226
- channelIcon: (_data$channel_info = data.channel_info) === null || _data$channel_info === void 0 ? void 0 : _data$channel_info.image_url,
227
- end: data.end,
228
- section: {
229
- id: data.section_id,
230
- start: data.start_time,
231
- end: data.end_time
232
- },
233
- previous: data.prev_video,
234
- next: data.next_video,
235
- startTime: (_data$time = data.time) === null || _data$time === void 0 ? void 0 : _data$time.last_position,
236
- seekable: data.seekable,
237
- chapters: [{
238
- type: 'intro',
239
- startTime: 0
240
- }, ...getOpeningSkipChapters(data), ((_data$time2 = data.time) === null || _data$time2 === void 0 ? void 0 : _data$time2.end_start_position) && {
241
- type: 'ending',
242
- startTime: data.time.end_start_position
243
- }].filter(Boolean),
244
- skipUiUpdate: data.skipUiUpdate
245
- };
246
- };
247
-
248
- const on = (target, name, handler, ...rest) => {
249
- target.addEventListener(name, handler, ...rest);
250
- return () => target.removeEventListener(name, handler, ...rest);
251
- };
252
-
253
- const once = (target, name, handler) => {
254
- const oneTime = (...args) => {
255
- handler(...args);
256
- target.removeEventListener(name, oneTime);
257
- };
258
-
259
- target.addEventListener(name, oneTime);
260
- return () => target.removeEventListener(name, oneTime);
261
- };
262
-
263
- const EnvironmentErrorName = {
264
- NOT_SUPPORT_DEVICE: 'KKS.ERROR.DEVICE_IS_NOT_SUPPORTED',
265
- NOT_SUPPORT_OS: 'KKS.ERROR.OS_IS_NOT_SUPPORTED',
266
- NOT_SUPPORT_OS_VERSION: 'KKS.ERROR.PLEASE_UPGRADE_OS',
267
- NOT_SUPPORT_BROWSER: 'KKS.ERROR.BROWSER_IS_NOT_SUPPORTED',
268
- NOT_SUPPORT_BROWSER_VERSION: 'KKS.ERROR.PLEASE_UPGRADE_BROWSER'
269
- };
270
-
271
- /* eslint-disable no-plusplus */
272
- const parser = new UAParser();
273
- function getOS() {
274
- return parser.getOS();
275
- }
276
- function getDevice() {
277
- const device = parser.getDevice();
278
- const osName = getOS().name;
279
- if (device.type === undefined && osName === 'Android') device.type = 'tablet';
280
- return device;
281
- }
282
- function getBrowser() {
283
- return parser.getBrowser();
284
- }
285
-
286
- const isSafari = () => /^((?!chrome|android|X11|Linux).)*(safari|iPad|iPhone|Version)/i.test(navigator.userAgent);
287
-
288
- function needNativeHls() {
289
- // Don't let Android phones play HLS, even if some of them report supported
290
- // This covers Samsung & OPPO special cases
291
- const isAndroid = /android|X11|Linux/i.test(navigator.userAgent);
292
- return isAndroid || /firefox/i.test(navigator.userAgent) ? '' : // canPlayType isn't reliable across all iOS verion / device combinations, so also check user agent
293
- isSafari() ? 'maybe' : document.createElement('video').canPlayType('application/vnd.apple.mpegURL');
294
- }
295
-
296
- const isDesktop = () => !getDevice().type; // TODO solve lint error:
297
-
298
- function compareVersion(v1, v2) {
299
- if (!/\d+(\.\d+)*/.test(v1)) throw Error(`the version format ${v1} is wrong`);
300
- if (!/\d+(\.\d+)*/.test(v2)) throw Error(`the version format ${v2} is wrong`);
301
- const v1parts = v1.split('.').map(p => Number(p));
302
- const v2parts = v2.split('.').map(p => Number(p));
303
-
304
- for (let i = 0, I = Math.max(v1parts.length, v2parts.length); i < I; i++) {
305
- if (v1parts[i] !== v2parts[i]) {
306
- return (v1parts[i] || 0) - (v2parts[i] || 0);
307
- }
308
- }
309
-
310
- return 0;
311
- }
312
-
313
- const validateEnvironment = (supportEnvironmentList = []) => {
314
- if (supportEnvironmentList.length === 0) {
315
- return;
316
- }
317
-
318
- const device = getDevice();
319
- const os = getOS();
320
- const browser = getBrowser();
321
-
322
- const toUnique = list => Array.from(new Set(list));
323
-
324
- const validators = [{
325
- filter: ({
326
- device: {
327
- name,
328
- type
329
- }
330
- }) => name === '*' || type === 'desktop' && device.type === undefined || type === device.type,
331
- errorName: EnvironmentErrorName.NOT_SUPPORT_DEVICE,
332
- getErrorProps: list => ({
333
- allowDevices: toUnique(list.map(env => env.device.type))
334
- })
335
- }, {
336
- filter: ({
337
- os: {
338
- name
339
- }
340
- }) => name === '*' || name === os.name,
341
- errorName: EnvironmentErrorName.NOT_SUPPORT_OS,
342
- getErrorProps: list => ({
343
- allowOSs: toUnique(list.map(env => env.os.name))
344
- })
345
- }, {
346
- filter: ({
347
- os: {
348
- version
349
- }
350
- }) => version === '*' || compareVersion(os.version, version) >= 0,
351
- errorName: EnvironmentErrorName.NOT_SUPPORT_OS_VERSION,
352
- getErrorProps: list => ({
353
- minVersion: list[0].os.version
354
- })
355
- }, {
356
- filter: ({
357
- browser: {
358
- name
359
- }
360
- }) => name === browser.name,
361
- errorName: EnvironmentErrorName.NOT_SUPPORT_BROWSER,
362
- getErrorProps: list => ({
363
- allowBrowsers: toUnique(list.map(env => env.browser.name))
364
- })
365
- }, {
366
- filter: ({
367
- browser: {
368
- version
369
- }
370
- }) => compareVersion(browser.version, version) >= 0,
371
- errorName: EnvironmentErrorName.NOT_SUPPORT_BROWSER_VERSION,
372
- getErrorProps: list => ({
373
- minVersion: list[0].browser.version
374
- })
375
- }];
376
- let scopes = supportEnvironmentList;
377
-
378
- for (let i = 0; i < validators.length; i++) {
379
- const validator = validators[i];
380
- const newScopes = scopes.filter(validator.filter);
381
-
382
- if (newScopes.length === 0) {
383
- return {
384
- name: validator.errorName,
385
- ...validator.getErrorProps(scopes)
386
- };
387
- }
388
-
389
- scopes = newScopes;
390
- }
391
- }; // Some touch devices with a mouse can't be distinguished, assume no mouse
392
-
393
- const protocolExtensions = {
394
- hls: 'm3u8',
395
- dash: 'mpd'
396
- };
397
- const mimeTypes = {
398
- hls: 'application/x-mpegurl',
399
- dash: 'application/dash+xml'
400
- };
401
-
402
- const matchType = (source, manifestType) => {
403
- var _source$type, _source$type2, _ref, _ref$endsWith;
404
-
405
- return ((_source$type = source.type) === null || _source$type === void 0 ? void 0 : _source$type.toLowerCase().includes(manifestType)) || ((_source$type2 = source.type) === null || _source$type2 === void 0 ? void 0 : _source$type2.toLowerCase()) === mimeTypes[manifestType] || ((_ref = source.src || source) === null || _ref === void 0 ? void 0 : (_ref$endsWith = _ref.endsWith) === null || _ref$endsWith === void 0 ? void 0 : _ref$endsWith.call(_ref, protocolExtensions[manifestType]));
406
- };
407
-
408
- const getDrmOptions = fallbackDrm => {
409
- if (!(fallbackDrm !== null && fallbackDrm !== void 0 && fallbackDrm.url)) {
410
- return;
411
- }
412
-
413
- const drmOptions = {
414
- licenseUri: fallbackDrm.url,
415
- headers: fallbackDrm.headers
416
- };
417
- return {
418
- widevine: drmOptions,
419
- fairplay: { ...drmOptions,
420
- certificateUri: `${fallbackDrm.url}/fairplay_cert`,
421
- ...fallbackDrm.fairplay
422
- },
423
- playready: drmOptions
424
- };
425
- };
426
- /**
427
- * @typedef {{src: string, type: string}} SourceObject
428
- * @typedef {{hls: string, dash: string}} SourceObjectAlt backward compatiable form
429
- *
430
- * @param {SourceObject[]|SourceObject|SourceObjectAlt|string} sourceOptions
431
- * @param {{preferManifestType?: ('dash'|'hls'|'platform')}} options
432
- * @return {{src: string, type: string, drm: Object}}
433
- */
434
-
435
-
436
- const getSource = (sourceOptions, {
437
- preferManifestType,
438
- fallbackDrm
439
- } = {}) => {
440
- if (sourceOptions.dash || sourceOptions.hls) {
441
- const {
442
- dash,
443
- hls
444
- } = sourceOptions;
445
- return getSource([hls && {
446
- src: hls,
447
- type: mimeTypes.hls
448
- }, dash && {
449
- src: dash,
450
- type: mimeTypes.dash
451
- }].filter(Boolean), {
452
- preferManifestType,
453
- fallbackDrm
454
- });
455
- }
456
-
457
- if (!Array.isArray(sourceOptions)) {
458
- return getSource([sourceOptions], {
459
- preferManifestType,
460
- fallbackDrm
461
- });
462
- }
463
-
464
- if (fallbackDrm) {
465
- return getSource(sourceOptions.map(option => ({ ...(option.src ? option : {
466
- src: option
467
- }),
468
- drm: getDrmOptions(fallbackDrm)
469
- })), {
470
- preferManifestType
471
- });
472
- }
473
-
474
- const targetType = preferManifestType !== 'platform' ? preferManifestType : isSafari() ? 'hls' : 'dash';
475
- const matched = sourceOptions.find(source => matchType(source, targetType));
476
- const selected = matched || sourceOptions[0];
477
-
478
- if (!selected) {
479
- return;
480
- }
481
-
482
- const type = matched && preferManifestType === 'hls' && mimeTypes.hls;
483
- return { ...(selected.src ? selected : {
484
- src: selected
485
- }),
486
- type
487
- };
488
- };
489
-
490
- /* eslint-disable no-param-reassign */
491
-
492
- const SHAKA_LIVE_DURATION = 4294967296;
493
-
494
- const isLiveDuration = duration => duration >= SHAKA_LIVE_DURATION;
495
-
496
- var _window, _window$localStorage;
497
-
498
- /**
499
- * @typedef {{
500
- * DRM_PROTAL_URL?: string
501
- * }} debugOption
502
- */
503
-
504
- /**
505
- * @type {debugOption}
506
- */
507
- const DEBUG_OPTIONS = {
508
- DRM_PROTAL_URL: typeof window !== 'undefined' ? (_window = window) === null || _window === void 0 ? void 0 : (_window$localStorage = _window.localStorage) === null || _window$localStorage === void 0 ? void 0 : _window$localStorage.DRM_PROTAL_URL : ""
509
- };
510
-
511
- /* eslint-disable no-param-reassign */
512
- const getCache = async (cache, key, {
513
- load,
514
- isValid
515
- }) => {
516
- if (!cache) {
517
- return load();
518
- }
519
-
520
- if (cache[key] && (!isValid || isValid(cache[key]))) {
521
- return cache[key];
522
- }
523
-
524
- if (!cache[key]) {
525
- cache[key] = {};
526
- }
527
-
528
- cache[key] = { ...cache[key],
529
- ...(await load())
530
- };
531
- return cache[key];
532
- };
533
-
534
- var getCache$1 = getCache;
535
-
536
- /* eslint-disable no-param-reassign */
537
-
538
- const deepEqual = (current, updated) => JSON.stringify(current) === JSON.stringify(updated);
539
-
540
- const HEARTBEAT_INTERVAL_MS$1 = 10000;
541
- const UPDATE_INTERVAL_MS = 10000; // If content.end_time is `undefined`, like `videos`, we never use cache.
542
-
543
- const isFreshCache = content => !content.end && Date.now() <= content.end_time * 1000; // TODO: separate by host
544
-
545
-
546
- let lastSession = Promise.resolve();
547
-
548
- const programRemainTime = (content, {
549
- media = {}
550
- }) => {
551
- const programCurrentTime = isLiveDuration(media.duration) ? Date.now() / 1000 : content.start_time + media.currentTime;
552
- return content.end_time - programCurrentTime;
553
- };
554
-
555
- const startPlaybackSession = async (playbackApi, options = {}) => {
556
- var _state$content2;
557
-
558
- await Promise.race([lastSession, new Promise(resolve => {
559
- setTimeout(resolve, UPDATE_INTERVAL_MS / 2);
560
- })]);
561
- let unlockSession;
562
- lastSession = new Promise(resolve => {
563
- unlockSession = resolve;
564
- });
565
- const emitter = mitt();
566
- const {
567
- type,
568
- id,
569
- getCurrentTime,
570
- cache
571
- } = options;
572
- const cacheKey = options.key || `${type}/${id}`;
573
- const {
574
- onChangeContent,
575
- onSessionStart,
576
- onReset,
577
- heartbeatTime = HEARTBEAT_INTERVAL_MS$1,
578
- updateTime = UPDATE_INTERVAL_MS
579
- } = options;
580
- const state = {
581
- content: {
582
- end_time: Date.now() / 1000
583
- },
584
- currentContent: {
585
- end_time: Date.now() / 1000
586
- }
587
- };
588
-
589
- const updateContent = async ({
590
- useCache
591
- } = {}) => {
592
- var _state$content;
593
-
594
- const content = await getCache$1(cache, cacheKey, {
595
- load: () => playbackApi.getContent({
596
- type,
597
- id
598
- }),
599
- isValid: data => useCache && isFreshCache(data)
600
- });
601
-
602
- if (content.end) {
603
- emitter.emit('liveEnd', content);
604
- }
605
-
606
- if (!((_state$content = state.content) !== null && _state$content !== void 0 && _state$content.end_time) && content !== null && content !== void 0 && content.end_time) {
607
- // transition from non-scheduled program to scheduled
608
- return onReset({
609
- reason: 'program start',
610
- sourceChange: true
611
- });
612
- }
613
-
614
- const timeBeforeEnd = programRemainTime(state.currentContent, options) * 1000;
615
-
616
- if (timeBeforeEnd > 0 && timeBeforeEnd <= UPDATE_INTERVAL_MS) {
617
- setTimeout(() => {
618
- if (programRemainTime(state.currentContent, options) > 0.5) {
619
- return;
620
- }
621
-
622
- if (isLiveDuration(options.media.duration)) {
623
- // request new content for next ip linear program
624
- updateContent({
625
- useCache: true
626
- });
627
- } else {
628
- onReset({
629
- reason: 'program end',
630
- sourceChange: true
631
- });
632
- }
633
- }, timeBeforeEnd);
634
- }
635
-
636
- if (!deepEqual(content, state.content)) {
637
- state.content = content; // keep watching current program
638
-
639
- const skipUiUpdate = !useCache;
640
-
641
- if (!skipUiUpdate) {
642
- state.currentContent = content;
643
- }
644
-
645
- onChangeContent === null || onChangeContent === void 0 ? void 0 : onChangeContent({
646
- type,
647
- ...content,
648
- skipUiUpdate
649
- });
650
- }
651
- }; // get last playback time to start playback fast
652
- // getContent is not critical, so don't block playback if it hangs or fails(ignored in API logic)
653
-
654
-
655
- const waitForContent = Promise.race([updateContent({
656
- useCache: true
657
- }), new Promise(resolve => {
658
- setTimeout(resolve, UPDATE_INTERVAL_MS);
659
- })]);
660
- const sessionInfo = await playbackApi.startPlayback({
661
- type,
662
- id
663
- }).catch(error => {
664
- unlockSession();
665
- return Promise.reject(error);
666
- });
667
- onSessionStart === null || onSessionStart === void 0 ? void 0 : onSessionStart(sessionInfo);
668
- const requestParams = {
669
- type,
670
- id,
671
- token: sessionInfo.token
672
- };
673
- state.token = sessionInfo.token;
674
- state.sources = (await getCache$1(cache, cacheKey, {
675
- load: () => playbackApi.getPlaybackInfo({
676
- type,
677
- id,
678
- token: state.token
679
- }),
680
- isValid: data => data.sources && isFreshCache(data)
681
- })).sources;
682
- let updateIntervalId;
683
-
684
- if (type === 'lives') {
685
- updateIntervalId = setInterval(updateContent, updateTime);
686
- }
687
-
688
- let lastPlayedTime;
689
-
690
- const updateLastPlayed = () => {
691
- const currentTime = getCurrentTime === null || getCurrentTime === void 0 ? void 0 : getCurrentTime();
692
-
693
- if (currentTime >= 0 && lastPlayedTime !== currentTime) {
694
- lastPlayedTime = currentTime;
695
- playbackApi.updateLastPlayed({ ...requestParams,
696
- time: currentTime
697
- });
698
- }
699
- };
700
-
701
- if (type === 'videos') {
702
- updateIntervalId = setInterval(updateLastPlayed, updateTime);
703
- }
704
-
705
- const heartbeatIntervalId = setInterval(() => playbackApi.heartbeat(requestParams).catch(error => {
706
- var _error$response;
707
-
708
- if (/4\d\d/.test((_error$response = error.response) === null || _error$response === void 0 ? void 0 : _error$response.status)) {
709
- clearInterval(heartbeatIntervalId);
710
- onReset === null || onReset === void 0 ? void 0 : onReset({
711
- sourceChange: false,
712
- reason: 'Invalid token'
713
- });
714
- }
715
- }), heartbeatTime);
716
-
717
- const end = ({
718
- preserveSource
719
- } = {}) => {
720
- if (!preserveSource && cache !== null && cache !== void 0 && cache[cacheKey]) {
721
- cache[cacheKey].sources = undefined;
722
- }
723
-
724
- updateLastPlayed();
725
- clearInterval(updateIntervalId);
726
- clearInterval(heartbeatIntervalId);
727
- clearTimeout(state.endTimeoutId);
728
- emitter.emit('playbackEnded');
729
- return playbackApi.endPlayback(requestParams).finally(unlockSession);
730
- };
731
-
732
- if ((_state$content2 = state.content) !== null && _state$content2 !== void 0 && _state$content2.end) {
733
- end();
734
- }
735
-
736
- emitter.on('liveEnd', end);
737
- await waitForContent;
738
- return { ...state,
739
- token: sessionInfo.token,
740
- drmPortalUrl: DEBUG_OPTIONS.DRM_PROTAL_URL || sessionInfo.drm_portal_url,
741
- updateLastPlayed,
742
- end,
743
- replaceApi: replacement => {
744
- playbackApi = replacement;
745
- }
746
- };
747
- };
748
-
749
- /* eslint-disable no-bitwise */
750
- const uuidv4 = () => {
751
- const crypto = window.crypto || window.msCrypto;
752
- return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, c => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16));
753
- };
754
-
755
- const modes = {
756
- videos: 'video',
757
- lives: 'live'
758
- };
759
13
  const logEventNames$1 = {
760
- playbackBegan: 'video_playback_began',
761
- playbackStarted: 'video_playback_started',
762
- playbackStopped: 'video_playback_stopped',
763
- playbackEnded: 'video_playback_ended',
764
- bufferingStarted: 'video_buffering_started',
765
- bufferingEnded: 'video_buffering_ended',
766
- playbackSpeedChange: 'playback_speed_change',
767
- seeked: 'video_seeking_ended',
768
- playbackError: 'video_playback_error_occurred',
769
- playing: 'play',
770
- paused: 'pause',
771
- rewind: 'rewind',
772
- forward: 'forward',
773
- speedSettingChange: 'speed_setting_change',
774
- previousEpisode: 'previous_episode',
775
- nextEpisode: 'next_episode',
776
- openSettings: 'setting_page_entered',
777
- closeSettings: 'setting_page_exited',
778
- adPlaybackStarted: 'ad_playback_started',
779
- adPlaybackStopped: 'ad_playback_stopped',
780
- skip: 'op_skip'
781
- };
782
-
783
- const mapLogEvents$1 = ({
784
- video,
785
- session = video,
786
- version,
787
- playerName,
788
- getPlaybackStatus = () => video
789
- }) => {
790
- var _session$getContent;
791
-
792
- const emitter = mitt();
793
- const state = {
794
- status: 'init',
795
- seeking: false,
796
- playerStartTime: Date.now(),
797
- moduleStartTime: Date.now(),
798
- content: ((_session$getContent = session.getContent) === null || _session$getContent === void 0 ? void 0 : _session$getContent.call(session)) || {},
799
- temporaryEvent: null
800
- };
801
-
802
- const commonProperties = () => {
803
- var _state$content$sectio;
804
-
805
- return {
806
- player_name: playerName,
807
- playback_module_version: version,
808
- playback_mode: modes[state.content.type],
809
- playback_session_id: state.sessionId,
810
- id: state.content.id,
811
- name: state.content.title,
812
- ...(state.content.type === 'videos' && {
813
- current_position: state.currentTime,
814
- video_total_duration: state.duration
815
- }),
816
- ...(state.content.type === 'lives' && {
817
- section_id: (_state$content$sectio = state.content.section) === null || _state$content$sectio === void 0 ? void 0 : _state$content$sectio.id,
818
- name_2: state.content.channelName,
819
- live_offset: state.liveOffset || 0
820
- }),
821
- SSAI: state.ssaiProvider || 'None'
822
- };
823
- };
824
-
825
- const dispatchStart = () => {
826
- if (state.status === 'started') {
827
- return;
828
- }
829
-
830
- state.status = 'started';
831
- state.lastStartTime = video.currentTime;
832
- const eventName = state.isPlayingAd ? 'adPlaybackStarted' : 'playbackStarted';
833
- state.temporaryEvent = eventName;
834
- };
835
-
836
- const dispatchStop = () => {
837
- if (state.status !== 'started') {
838
- return;
839
- }
840
-
841
- state.status = 'stopped';
842
- const played = video.currentTime - state.lastStartTime;
843
-
844
- if (state.isPlayingAd) {
845
- state.adPlayedDuration += played;
846
- } else {
847
- state.playedDuration += played;
848
- }
849
-
850
- const eventName = state.isPlayingAd ? 'adPlaybackStopped' : 'playbackStopped';
851
- const durationPropertyName = state.isPlayingAd ? 'ad_played_duration' : 'video_played_duration';
852
- emitter.emit(eventName, { ...commonProperties(),
853
- ...{
854
- [durationPropertyName]: played
855
- }
856
- });
857
- };
858
-
859
- const registered = [on(video, 'error', event => {
860
- var _event$error, _event$error2, _event$error2$data;
861
-
862
- emitter.emit('playbackError', {
863
- module_error_code: ((_event$error = event.error) === null || _event$error === void 0 ? void 0 : _event$error.code) || ((_event$error2 = event.error) === null || _event$error2 === void 0 ? void 0 : (_event$error2$data = _event$error2.data) === null || _event$error2$data === void 0 ? void 0 : _event$error2$data.code),
864
- ...commonProperties()
865
- });
866
- }), once(video, 'playerStarted', () => {
867
- state.playerStartTime = Date.now();
868
- }), on(video, 'durationchange', () => {
869
- // duration may change when playing an ad stitched stream, take only initial value
870
- if (!state.duration) {
871
- state.duration = getPlaybackStatus().duration;
872
- }
873
- }), once(video, 'loadeddata', () => {
874
- state.status = 'began';
875
- state.sessionId = uuidv4();
876
- state.playedDuration = 0;
877
- emitter.emit('playbackBegan', {
878
- player_startup_time: (state.playerStartTime - state.moduleStartTime) / 1000,
879
- video_startup_time: (Date.now() - state.moduleStartTime) / 1000,
880
- ...commonProperties()
881
- });
882
- }), on(video, 'playing', dispatchStart), on(video, 'waiting', () => {
883
- if (!state.bufferingStartTime) {
884
- emitter.emit('bufferingStarted', commonProperties());
885
- state.bufferingStartTime = Date.now();
886
- }
887
- }), on(video, 'timeupdate', () => {
888
- const status = getPlaybackStatus();
889
- state.currentTime = status.currentTime;
890
-
891
- if (state.content.type === 'lives') {
892
- state.liveOffset = status.liveOffset < 10 ? 0 : status.liveOffset;
893
- }
894
-
895
- if (state.bufferingStartTime) {
896
- emitter.emit('bufferingEnded', {
897
- buffering_second: (Date.now() - state.bufferingStartTime) / 1000,
898
- ...commonProperties()
899
- });
900
- state.bufferingStartTime = undefined;
901
- }
902
-
903
- if (state.temporaryEvent && !state.seeking) {
904
- emitter.emit(state.temporaryEvent, commonProperties());
905
- state.temporaryEvent = null;
906
- }
907
- }), on(video, 'pause', dispatchStop), on(video, 'seeking', () => {
908
- state.seeking = true;
909
- state.seekingFrom = state.currentTime;
910
- }), on(video, 'seeked', () => {
911
- emitter.emit('seeked', {
912
- seeking_from: state.seekingFrom,
913
- seeking_to: video.currentTime,
914
- ...commonProperties()
915
- });
916
- state.seeking = false;
917
- }), on(video, 'skip', data => {
918
- emitter.emit('skip', {
919
- skip_from: data.skipFrom,
920
- skip_to: data.skipTo,
921
- trigger_method: data.triggerMethod,
922
- ...commonProperties()
923
- });
924
- }), on(video, 'ratechange', () => {
925
- // monkey patch for shaka player legecy issue, for more info plz see https://github.com/shaka-project/shaka-player/issues/951
926
- if (video.playbackRate === 0) {
927
- return;
928
- }
929
-
930
- emitter.emit('playbackSpeedChange', {
931
- playbackSpeed: video.playbackRate,
932
- ...commonProperties()
933
- });
934
- }), on(session, 'sectionChange', () => {
935
- dispatchStop();
936
- state.content = session.getContent();
937
- dispatchStart();
938
- }), once(video, 'ended', () => {
939
- if (state.status === 'started') {
940
- dispatchStop();
941
- }
942
-
943
- state.status = 'init';
944
- emitter.emit('playbackEnded', {
945
- video_playback_ended_at_percentage: state.currentTime / state.duration,
946
- video_total_played_duration: state.playedDuration,
947
- ...(state.ssaiProvider && {
948
- ad_total_played_duration: state.adPlayedDuration
949
- }),
950
- ...commonProperties()
951
- });
952
- }), once(video, 'loadedAdMetadata', event => {
953
- state.ssaiProvider = event.data.provider;
954
- state.adPlayedDuration = 0;
955
- }), on(session, 'adBreakStarted', () => {
956
- dispatchStop();
957
- state.isPlayingAd = true;
958
-
959
- if (!state.seeking) {
960
- dispatchStart();
961
- }
962
- }), on(session, 'adBreakEnded', () => {
963
- dispatchStop();
964
- state.isPlayingAd = false;
965
-
966
- if (!state.seeking) {
967
- dispatchStart();
968
- }
969
- })];
970
- return {
971
- addEventListener: (name, handler) => emitter.on(name, handler),
972
- all: handler => emitter.on('*', handler),
973
- emit: (name, {
974
- currentTime
975
- }, properties) => {
976
- if (name in logEventNames$1) {
977
- emitter.emit(name, {
978
- current_position: currentTime,
979
- ...properties,
980
- ...commonProperties()
981
- });
982
- }
983
- },
984
- updateContent: content => {
985
- state.content = content;
986
- },
987
- reset: () => registered.forEach(off => off())
988
- };
989
- };
990
-
991
- function getVersion() {
992
- try {
993
- // eslint-disable-next-line no-undef
994
- return "2.25.0-canary.24";
995
- } catch (e) {
996
- return undefined;
997
- }
998
- }
999
-
1000
- const logEventNames = {
1001
- playbackBeganLoading: 'playback_began_player_loading',
1002
- playbackBeganPlayerStartupTime: 'playback_began_player_startup_time',
1003
- playbackBeganVideoStartupTime: 'playback_began_video_startup_time',
1004
- playbackVideoBegan: 'playback_video_began',
1005
- playbackVideoPaused: 'playback_video_paused',
1006
- playbackVideoBufferingBegan: 'playback_video_buffering_began',
1007
- playbackVideoBufferingEnded: 'playback_video_buffering_ended',
1008
- playbackVideoEnded: 'playback_video_ended',
1009
- playbackSeekingBegan: 'playback_seeking_began',
1010
- playbackSeekingEnded: 'playback_seeking_ended',
1011
- playbackError: 'playback_error_occurred',
1012
- playbackSpeedChange: 'playback_speed_change',
1013
- playbackAudioVolumeChange: 'playback_audio_volume_change',
1014
- playbackAudioMuteChange: 'playback_audio_mute_change',
1015
- playbackStreamingQualityChangeDownload: 'playback_streaming_quality_change_download',
1016
- playbackStreamingQualityChangeRender: 'playback_streaming_quality_change_render',
1017
- playbackAudioTrackChange: 'playback_audio_track_change',
1018
- playbackSubtitleChange: 'playback_subtitle_change',
1019
- playbackLoopChange: 'playback_loop_change',
1020
- playing: 'play',
1021
- paused: 'pause',
1022
- seek: 'seek',
1023
- rewind: 'rewind',
1024
- forward: 'forward',
1025
- openSettings: 'setting_page_entered',
1026
- closeSettings: 'setting_page_exited',
1027
- speedSettingChange: 'speed_setting_change',
1028
- qualitySettingChange: 'quality_setting_change',
1029
- audioVolumeSettingChange: 'audio_volume_setting_change',
1030
- audioMuteSettingChange: 'audio_mute_setting_change',
1031
- audioTrackSettingChange: 'audio_track_setting_change',
1032
- subtitleSettingChange: 'subtitle_setting_change',
1033
- loopSettingChange: 'loop_setting_change'
1034
- };
1035
-
1036
- const mapLogEvents = ({
1037
- video,
1038
- version,
1039
- playerName,
1040
- getPlaybackStatus = () => video
1041
- }) => {
1042
- const emitter = mitt();
1043
- const state = {
1044
- status: 'init',
1045
- seeking: false,
1046
- volume: undefined,
1047
- muted: undefined,
1048
- loop: undefined,
1049
- lastPlaybackRate: 1,
1050
- hasEnabledSubtitle: false,
1051
- resourceType: undefined
1052
- };
1053
-
1054
- const baseProperties = () => ({
1055
- player_name: playerName,
1056
- playback_module_version: version || getVersion(),
1057
- system_time: Date.now() / 1000
1058
- });
1059
-
1060
- const commonProperties = () => ({ ...baseProperties(),
1061
- current_position: state.currentTime,
1062
- resource_type: state.resourceType
1063
- });
1064
-
1065
- const dispatchStart = () => {
1066
- if (state.status === 'started') {
1067
- return;
1068
- }
1069
-
1070
- state.status = 'started';
1071
- emitter.emit('playbackVideoBegan', commonProperties());
1072
- };
1073
-
1074
- const dispatchStop = () => {
1075
- if (state.status !== 'started') {
1076
- return;
1077
- }
1078
-
1079
- state.status = 'stopped';
1080
- emitter.emit('playbackVideoPaused', commonProperties());
1081
- };
1082
-
1083
- const handleEnd = () => {
1084
- if (state.status === 'started') {
1085
- dispatchStop();
1086
- }
1087
-
1088
- if (state.status !== 'init') {
1089
- state.status = 'init';
1090
- emitter.emit('playbackVideoEnded', commonProperties());
1091
- }
1092
- };
1093
-
1094
- const playbackEvents = [on(video, 'error', event => {
1095
- var _event$error, _event$error2, _event$error2$data;
1096
-
1097
- emitter.emit('playbackError', {
1098
- error_code: ((_event$error = event.error) === null || _event$error === void 0 ? void 0 : _event$error.code) || ((_event$error2 = event.error) === null || _event$error2 === void 0 ? void 0 : (_event$error2$data = _event$error2.data) === null || _event$error2$data === void 0 ? void 0 : _event$error2$data.code),
1099
- ...commonProperties()
1100
- });
1101
- }), on(video, 'durationchange', () => {
1102
- // duration may change when playing an ad stitched stream, take only initial value
1103
- if (!state.duration) {
1104
- state.duration = getPlaybackStatus().duration;
1105
- }
1106
- }), once(video, 'playerStarted', () => {
1107
- emitter.emit('playbackBeganLoading', baseProperties());
1108
- }), once(video, 'loadstart', () => {
1109
- emitter.emit('playbackBeganPlayerStartupTime', baseProperties());
1110
- }), once(video, 'canplay', () => {
1111
- state.status = 'began';
1112
- state.resourceType = isLiveDuration(video.duration) ? 'live' : 'vod';
1113
- emitter.emit('playbackBeganVideoStartupTime', { ...baseProperties(),
1114
- resource_type: state.resourceType
1115
- }); // sync state from video
1116
-
1117
- state.volume = video.volume;
1118
- state.muted = video.muted;
1119
- }), on(video, 'playing', dispatchStart), on(video, 'waiting', () => {
1120
- if (!state.buffering) {
1121
- emitter.emit('playbackVideoBufferingBegan', commonProperties());
1122
- state.buffering = true;
1123
- }
1124
- }), on(video, 'timeupdate', () => {
1125
- state.currentTime = getPlaybackStatus().currentTime;
1126
-
1127
- if (state.buffering) {
1128
- emitter.emit('playbackVideoBufferingEnded', commonProperties());
1129
- state.buffering = false;
1130
- }
1131
- }), on(video, 'pause', dispatchStop), on(video, 'seeking', () => {
1132
- state.seeking = true;
1133
- emitter.emit('playbackSeekingBegan', commonProperties());
1134
- }), on(video, 'seeked', () => {
1135
- if (state.seeking) {
1136
- emitter.emit('playbackSeekingEnded', commonProperties());
1137
- }
1138
-
1139
- state.seeking = false;
1140
- }), on(video, 'ratechange', () => {
1141
- /*
1142
- Monkey patch for shaka player legecy issue:
1143
- For more info plz see https://github.com/shaka-project/shaka-player/issues/951
1144
- `video.playbackRate` may be 0.
1145
- */
1146
- const {
1147
- lastPlaybackRate
1148
- } = state;
1149
- state.lastPlaybackRate = video.playbackRate || state.lastPlaybackRate;
1150
-
1151
- if (lastPlaybackRate === state.lastPlaybackRate) {
1152
- return;
1153
- }
1154
-
1155
- state.lastPlaybackRate = video.playbackRate;
1156
- emitter.emit('playbackSpeedChange', {
1157
- playback_speed: video.playbackRate,
1158
- ...commonProperties()
1159
- });
1160
- }), on(video, 'loopChange', () => {
1161
- if (video.loop === state.loop) {
1162
- return;
1163
- }
1164
-
1165
- emitter.emit('playbackLoopChange', {
1166
- loop: video.loop,
1167
- ...commonProperties()
1168
- });
1169
- state.loop = video.loop;
1170
- }), on(video, 'ended', handleEnd), on(video, 'volumechange', () => {
1171
- if (video.volume !== state.volume && state.volume !== undefined) {
1172
- emitter.emit('playbackAudioVolumeChange', {
1173
- volume: video.volume,
1174
- ...commonProperties()
1175
- });
1176
- state.volume = video.volume;
1177
- }
1178
-
1179
- if (video.muted !== state.muted && state.muted !== undefined) {
1180
- emitter.emit('playbackAudioMuteChange', {
1181
- muted: video.muted,
1182
- ...commonProperties()
1183
- });
1184
- state.muted = video.muted;
1185
- }
1186
- }), on(video, 'downloadQualityChange', event => {
1187
- emitter.emit('playbackStreamingQualityChangeDownload', { ...event.detail,
1188
- ...commonProperties()
1189
- });
1190
- }), on(video, 'audioTrackChange', event => {
1191
- emitter.emit('playbackAudioTrackChange', { ...event.detail,
1192
- ...commonProperties()
1193
- });
1194
- }), on(video, 'textTrackChange', event => {
1195
- var _event$detail$getText, _event$detail;
1196
-
1197
- const activeTrack = ((_event$detail$getText = (_event$detail = event.detail).getTextTracks) === null || _event$detail$getText === void 0 ? void 0 : _event$detail$getText.call(_event$detail).find(track => track.active)) || {
1198
- language: 'off',
1199
- name: 'off'
1200
- };
1201
-
1202
- if (!state.hasEnabledSubtitle && activeTrack.language === 'off') {
1203
- return;
1204
- }
1205
-
1206
- state.hasEnabledSubtitle = activeTrack.language !== 'off';
1207
- emitter.emit('playbackSubtitleChange', {
1208
- lang: activeTrack.language,
1209
- // TODO also add display name here
1210
- ...commonProperties()
1211
- });
1212
- }), on(video, 'resize', () => {
1213
- emitter.emit('playbackStreamingQualityChangeRender', {
1214
- height: video.videoHeight,
1215
- width: video.videoWidth,
1216
- ...commonProperties()
1217
- });
1218
- })];
1219
- const uiEvents = [on(video, 'audioVolumeSettingChange', event => {
1220
- var _event$detail2;
1221
-
1222
- emitter.emit('audioVolumeSettingChange', {
1223
- volume: (_event$detail2 = event.detail) === null || _event$detail2 === void 0 ? void 0 : _event$detail2.volume,
1224
- ...commonProperties()
1225
- });
1226
- })];
1227
- const registered = playbackEvents.concat(uiEvents);
1228
- return {
1229
- addEventListener: (name, handler) => emitter.on(name, handler),
1230
- all: handler => emitter.on('*', handler),
1231
-
1232
- /**
1233
- * @param
1234
- * @param {{currentTime?: string}}
1235
- * */
1236
- emit: (name, {
1237
- currentTime
1238
- }, properties) => {
1239
- emitter.emit(name, {
1240
- current_position: currentTime,
1241
- ...properties,
1242
- ...commonProperties()
1243
- });
1244
- },
1245
- updateContent: content => {
1246
- state.content = content;
1247
- },
1248
- getCommonProperties: () => commonProperties(),
1249
- reset: () => {
1250
- registered.forEach(off => off());
1251
- handleEnd();
1252
- }
1253
- };
1254
- };
1255
-
1256
- var playlogv3 = /*#__PURE__*/Object.freeze({
1257
- __proto__: null,
1258
- mapLogEvents: mapLogEvents,
1259
- logEventNames: logEventNames
1260
- });
1261
-
1262
- const waitMs = time => new Promise(resolve => {
1263
- setTimeout(resolve, time);
14
+ playbackBeganLoading: "playback_began_player_loading",
15
+ playbackBeganPlayerStartupTime: "playback_began_player_startup_time",
16
+ playbackBeganVideoStartupTime: "playback_began_video_startup_time",
17
+ playbackVideoBegan: "playback_video_began",
18
+ playbackVideoPaused: "playback_video_paused",
19
+ playbackVideoBufferingBegan: "playback_video_buffering_began",
20
+ playbackVideoBufferingEnded: "playback_video_buffering_ended",
21
+ playbackVideoEnded: "playback_video_ended",
22
+ playbackSeekingBegan: "playback_seeking_began",
23
+ playbackSeekingEnded: "playback_seeking_ended",
24
+ playbackError: "playback_error_occurred",
25
+ playbackSpeedChange: "playback_speed_change",
26
+ playbackAudioVolumeChange: "playback_audio_volume_change",
27
+ playbackAudioMuteChange: "playback_audio_mute_change",
28
+ playbackStreamingQualityChangeDownload: "playback_streaming_quality_change_download",
29
+ playbackStreamingQualityChangeRender: "playback_streaming_quality_change_render",
30
+ playbackAudioTrackChange: "playback_audio_track_change",
31
+ playbackSubtitleChange: "playback_subtitle_change",
32
+ playbackLoopChange: "playback_loop_change",
33
+ playing: "play",
34
+ paused: "pause",
35
+ seek: "seek",
36
+ rewind: "rewind",
37
+ forward: "forward",
38
+ openSettings: "setting_page_entered",
39
+ closeSettings: "setting_page_exited",
40
+ speedSettingChange: "speed_setting_change",
41
+ qualitySettingChange: "quality_setting_change",
42
+ audioVolumeSettingChange: "audio_volume_setting_change",
43
+ audioMuteSettingChange: "audio_mute_setting_change",
44
+ audioTrackSettingChange: "audio_track_setting_change",
45
+ subtitleSettingChange: "subtitle_setting_change",
46
+ loopSettingChange: "loop_setting_change"
47
+ };
48
+ const mapLogEvents$1 = ({ video, version, playerName, getPlaybackStatus = () => video }) => {
49
+ const emitter = mitt();
50
+ const state = {
51
+ status: "init",
52
+ seeking: false,
53
+ volume: void 0,
54
+ muted: void 0,
55
+ loop: void 0,
56
+ lastPlaybackRate: 1,
57
+ hasEnabledSubtitle: false,
58
+ resourceType: void 0
59
+ };
60
+ const baseProperties = () => ({
61
+ player_name: playerName,
62
+ playback_module_version: version || getVersion(),
63
+ system_time: Date.now() / 1e3
64
+ });
65
+ const commonProperties = () => ({
66
+ ...baseProperties(),
67
+ current_position: state.currentTime,
68
+ resource_type: state.resourceType
69
+ });
70
+ const dispatchStart = () => {
71
+ if (state.status === "started") return;
72
+ state.status = "started";
73
+ emitter.emit("playbackVideoBegan", commonProperties());
74
+ };
75
+ const dispatchStop = () => {
76
+ if (state.status !== "started") return;
77
+ state.status = "stopped";
78
+ emitter.emit("playbackVideoPaused", commonProperties());
79
+ };
80
+ const handleEnd = () => {
81
+ if (state.status === "started") dispatchStop();
82
+ if (state.status !== "init") {
83
+ state.status = "init";
84
+ emitter.emit("playbackVideoEnded", commonProperties());
85
+ }
86
+ };
87
+ const playbackEvents = [
88
+ on(video, "error", (event) => {
89
+ emitter.emit("playbackError", {
90
+ error_code: event.error?.code || event.error?.data?.code,
91
+ ...commonProperties()
92
+ });
93
+ }),
94
+ on(video, "durationchange", () => {
95
+ if (!state.duration) state.duration = getPlaybackStatus().duration;
96
+ }),
97
+ once(video, "playerStarted", () => {
98
+ emitter.emit("playbackBeganLoading", baseProperties());
99
+ }),
100
+ once(video, "loadstart", () => {
101
+ emitter.emit("playbackBeganPlayerStartupTime", baseProperties());
102
+ }),
103
+ once(video, "canplay", () => {
104
+ state.status = "began";
105
+ state.resourceType = isLiveDuration(video.duration) ? "live" : "vod";
106
+ emitter.emit("playbackBeganVideoStartupTime", {
107
+ ...baseProperties(),
108
+ resource_type: state.resourceType
109
+ });
110
+ state.volume = video.volume;
111
+ state.muted = video.muted;
112
+ }),
113
+ on(video, "playing", dispatchStart),
114
+ on(video, "waiting", () => {
115
+ if (!state.buffering) {
116
+ emitter.emit("playbackVideoBufferingBegan", commonProperties());
117
+ state.buffering = true;
118
+ }
119
+ }),
120
+ on(video, "timeupdate", () => {
121
+ state.currentTime = getPlaybackStatus().currentTime;
122
+ if (state.buffering) {
123
+ emitter.emit("playbackVideoBufferingEnded", commonProperties());
124
+ state.buffering = false;
125
+ }
126
+ }),
127
+ on(video, "pause", dispatchStop),
128
+ on(video, "seeking", () => {
129
+ state.seeking = true;
130
+ emitter.emit("playbackSeekingBegan", commonProperties());
131
+ }),
132
+ on(video, "seeked", () => {
133
+ if (state.seeking) emitter.emit("playbackSeekingEnded", commonProperties());
134
+ state.seeking = false;
135
+ }),
136
+ on(video, "ratechange", () => {
137
+ const { lastPlaybackRate } = state;
138
+ state.lastPlaybackRate = video.playbackRate || state.lastPlaybackRate;
139
+ if (lastPlaybackRate === state.lastPlaybackRate) return;
140
+ state.lastPlaybackRate = video.playbackRate;
141
+ emitter.emit("playbackSpeedChange", {
142
+ playback_speed: video.playbackRate,
143
+ ...commonProperties()
144
+ });
145
+ }),
146
+ on(video, "loopChange", () => {
147
+ if (video.loop === state.loop) return;
148
+ emitter.emit("playbackLoopChange", {
149
+ loop: video.loop,
150
+ ...commonProperties()
151
+ });
152
+ state.loop = video.loop;
153
+ }),
154
+ on(video, "ended", handleEnd),
155
+ on(video, "volumechange", () => {
156
+ if (video.volume !== state.volume && state.volume !== void 0) {
157
+ emitter.emit("playbackAudioVolumeChange", {
158
+ volume: video.volume,
159
+ ...commonProperties()
160
+ });
161
+ state.volume = video.volume;
162
+ }
163
+ if (video.muted !== state.muted && state.muted !== void 0) {
164
+ emitter.emit("playbackAudioMuteChange", {
165
+ muted: video.muted,
166
+ ...commonProperties()
167
+ });
168
+ state.muted = video.muted;
169
+ }
170
+ }),
171
+ on(video, "downloadQualityChange", (event) => {
172
+ emitter.emit("playbackStreamingQualityChangeDownload", {
173
+ ...event.detail,
174
+ ...commonProperties()
175
+ });
176
+ }),
177
+ on(video, "audioTrackChange", (event) => {
178
+ emitter.emit("playbackAudioTrackChange", {
179
+ ...event.detail,
180
+ ...commonProperties()
181
+ });
182
+ }),
183
+ on(video, "textTrackChange", (event) => {
184
+ const activeTrack = event.detail.getTextTracks?.().find((track) => track.active) || {
185
+ language: "off",
186
+ name: "off"
187
+ };
188
+ if (!state.hasEnabledSubtitle && activeTrack.language === "off") return;
189
+ state.hasEnabledSubtitle = activeTrack.language !== "off";
190
+ emitter.emit("playbackSubtitleChange", {
191
+ lang: activeTrack.language,
192
+ ...commonProperties()
193
+ });
194
+ }),
195
+ on(video, "resize", () => {
196
+ emitter.emit("playbackStreamingQualityChangeRender", {
197
+ height: video.videoHeight,
198
+ width: video.videoWidth,
199
+ ...commonProperties()
200
+ });
201
+ })
202
+ ];
203
+ const uiEvents = [on(video, "audioVolumeSettingChange", (event) => {
204
+ emitter.emit("audioVolumeSettingChange", {
205
+ volume: event.detail?.volume,
206
+ ...commonProperties()
207
+ });
208
+ })];
209
+ const registered = playbackEvents.concat(uiEvents);
210
+ return {
211
+ addEventListener: (name, handler) => emitter.on(name, handler),
212
+ all: (handler) => emitter.on("*", handler),
213
+ emit: (name, { currentTime }, properties) => {
214
+ emitter.emit(name, {
215
+ current_position: currentTime,
216
+ ...properties,
217
+ ...commonProperties()
218
+ });
219
+ },
220
+ updateContent: (content) => {
221
+ state.content = content;
222
+ },
223
+ getCommonProperties: () => commonProperties(),
224
+ reset: () => {
225
+ registered.forEach((off) => off());
226
+ handleEnd();
227
+ }
228
+ };
229
+ };
230
+
231
+ //#endregion
232
+ //#region src/modules/wait.js
233
+ const waitMs = (time) => new Promise((resolve) => {
234
+ setTimeout(resolve, time);
1264
235
  });
1265
-
1266
- const retryWithBackoff = async ({
1267
- fn,
1268
- maxRetries = 10,
1269
- initialDelay = 1000
1270
- }) => {
1271
- let retryDelay = initialDelay;
1272
-
1273
- for (let retry = 1; retry <= maxRetries; retry++) {
1274
- try {
1275
- return await fn();
1276
- } catch (error) {
1277
- console.error(`Retry #${retry} failed:`, error);
1278
-
1279
- if (retry === maxRetries) {
1280
- console.error('Max retries reached. Giving up.');
1281
- throw error;
1282
- }
1283
-
1284
- await waitMs(retryDelay);
1285
- retryDelay += retry * 1000;
1286
- }
1287
- }
1288
- };
1289
-
1290
- var retryWithBackoff$1 = retryWithBackoff;
1291
-
236
+ var wait_default = waitMs;
237
+
238
+ //#endregion
239
+ //#region src/modules/retryWithBackoff.js
240
+ const retryWithBackoff = async ({ fn, maxRetries = 10, initialDelay = 1e3 }) => {
241
+ let retryDelay = initialDelay;
242
+ for (let retry = 1; retry <= maxRetries; retry++) try {
243
+ return await fn();
244
+ } catch (error) {
245
+ console.error(`Retry #${retry} failed:`, error);
246
+ if (retry === maxRetries) {
247
+ console.error("Max retries reached. Giving up.");
248
+ throw error;
249
+ }
250
+ await wait_default(retryDelay);
251
+ retryDelay += retry * 1e3;
252
+ }
253
+ };
254
+ var retryWithBackoff_default = retryWithBackoff;
255
+
256
+ //#endregion
257
+ //#region src/modules/retrieveModuleConfig.ts
1292
258
  const retrieveModuleConfig = (modulesConfig, query) => {
1293
- const result = {};
1294
- Object.entries(modulesConfig).forEach(([key, value]) => {
1295
- if (key.startsWith(`${query}.`)) {
1296
- const targetKey = key.replace(`${query}.`, '');
1297
- result[targetKey] = value;
1298
- }
1299
- });
1300
- return result;
1301
- };
1302
-
1303
- /* eslint-disable camelcase */
1304
- const HEARTBEAT_INTERVAL_MS = 10000;
1305
- //@ts-ignore
1306
- const axios$1 = {
1307
- post: (...args) => {
1308
- console.log(args);
1309
- }
1310
- }; // TODO
1311
-
1312
- const eventHeartbeat = ({
1313
- token,
1314
- resourceId,
1315
- sessionId,
1316
- userId,
1317
- deviceId,
1318
- rawData,
1319
- endpoint = process.env.BV_ONE_EVENT_HEARTBEAT_API + '/v1/events',
1320
- resourceTypes
1321
- }) => {
1322
- if (!token) {
1323
- return;
1324
- }
1325
-
1326
- const resourceType = rawData.analyticsConfig.resource_type || resourceTypes[rawData.logTarget.getCommonProperties().resource_type || 'vod'];
1327
- return setInterval(async () => {
1328
- const payload = {
1329
- event_type: 'heartbeat_log',
1330
- event_name: 'playback_player_active',
1331
- event_content: JSON.stringify({
1332
- resource_type: resourceType,
1333
- resource_id: resourceId,
1334
- session_id: sessionId,
1335
- device_id: deviceId,
1336
- user_id: userId,
1337
- ...rawData.analyticsConfig
1338
- }),
1339
- event_time: new Date().toISOString()
1340
- }; // Avoid template literals because the env variable would be replaced in the rollup
1341
-
1342
- await retryWithBackoff$1({
1343
- fn: () => axios$1.post(endpoint, payload, {
1344
- headers: {
1345
- 'Kks-Bv-Token': token
1346
- }
1347
- })
1348
- });
1349
- }, HEARTBEAT_INTERVAL_MS);
1350
- };
1351
-
1352
- var eventHeartbeat$1 = eventHeartbeat;
1353
-
1354
- /* eslint-disable camelcase */
1355
-
1356
- const axios = {
1357
- post: (...args) => {
1358
- console.log(args);
1359
- }
1360
- }; // TODO
1361
-
259
+ const result = {};
260
+ Object.entries(modulesConfig).forEach(([key, value]) => {
261
+ if (key.startsWith(`${query}.`)) {
262
+ const targetKey = key.replace(`${query}.`, "");
263
+ result[targetKey] = value;
264
+ }
265
+ });
266
+ return result;
267
+ };
268
+
269
+ //#endregion
270
+ //#region src/modules/heartbeat.ts
271
+ const HEARTBEAT_INTERVAL_MS = 1e4;
272
+ const axios$1 = { post: (...args) => {
273
+ console.log(args);
274
+ } };
275
+ const eventHeartbeat = ({ token, resourceId, sessionId, userId, deviceId, rawData, endpoint = process.env.BV_ONE_EVENT_HEARTBEAT_API + "/v1/events", resourceTypes: resourceTypes$1 }) => {
276
+ if (!token) return;
277
+ const resourceType = rawData.analyticsConfig.resource_type || resourceTypes$1[rawData.logTarget.getCommonProperties().resource_type || "vod"];
278
+ return setInterval(async () => {
279
+ const payload = {
280
+ event_type: "heartbeat_log",
281
+ event_name: "playback_player_active",
282
+ event_content: JSON.stringify({
283
+ resource_type: resourceType,
284
+ resource_id: resourceId,
285
+ session_id: sessionId,
286
+ device_id: deviceId,
287
+ user_id: userId,
288
+ ...rawData.analyticsConfig
289
+ }),
290
+ event_time: (/* @__PURE__ */ new Date()).toISOString()
291
+ };
292
+ await retryWithBackoff_default({ fn: () => axios$1.post(endpoint, payload, { headers: { "Kks-Bv-Token": token } }) });
293
+ }, HEARTBEAT_INTERVAL_MS);
294
+ };
295
+ var heartbeat_default = eventHeartbeat;
296
+
297
+ //#endregion
298
+ //#region src/modules/analytics.ts
299
+ const axios = { post: (...args) => {
300
+ console.log(args);
301
+ } };
1362
302
  const getDeviceInfo = () => {
1363
- const browser = getBrowser();
1364
- return {
1365
- browser: `${browser.name} ${browser.version}`,
1366
- device_category: isDesktop() ? 'Desktop' : 'Mobile',
1367
- device_platform: getOS().name
1368
- };
1369
- };
1370
-
1371
- const DEVICE_ID_KEY = 'kks-device-id';
1372
-
303
+ const browser = getBrowser();
304
+ return {
305
+ browser: `${browser.name} ${browser.version}`,
306
+ device_category: isDesktop() ? "Desktop" : "Mobile",
307
+ device_platform: getOS().name
308
+ };
309
+ };
310
+ const DEVICE_ID_KEY = "kks-device-id";
1373
311
  const getDefaultDeviceId = () => {
1374
- const deviceId = window.localStorage.getItem(DEVICE_ID_KEY) || uuidv4();
1375
- window.localStorage.setItem(DEVICE_ID_KEY, deviceId);
1376
- return deviceId;
312
+ const deviceId = window.localStorage.getItem(DEVICE_ID_KEY) || uuidv4_default();
313
+ window.localStorage.setItem(DEVICE_ID_KEY, deviceId);
314
+ return deviceId;
1377
315
  };
1378
-
1379
316
  const resourceTypes = {
1380
- vod: 'RESOURCE_TYPE_VOD_EVENT',
1381
- live: 'RESOURCE_TYPE_LIVE_EVENT'
317
+ vod: "RESOURCE_TYPE_VOD_EVENT",
318
+ live: "RESOURCE_TYPE_LIVE_EVENT"
1382
319
  };
1383
320
  /**
1384
- * This method creates analytics module instance and return it.
1385
- * @param createAnalyticsParameter
1386
- * @returns Analytics module instance
1387
- */
1388
-
1389
- const createAnalytics = ({
1390
- video,
1391
- onPlaylogFired,
1392
- modulesConfig
1393
- }) => {
1394
- const {
1395
- token,
1396
- eventEndpoint,
1397
- heartbeatEndpoint,
1398
- ...analyticsConfig
1399
- } = retrieveModuleConfig(modulesConfig, 'analytics');
1400
- const resourceId = analyticsConfig.resourceId || analyticsConfig.resource_id || '';
1401
- const resourceType = analyticsConfig.resourceType || analyticsConfig.resource_type || '';
1402
- const userId = analyticsConfig.userId || analyticsConfig.user_id || '';
1403
- const sessionId = analyticsConfig.sessionId || analyticsConfig.session_id || uuidv4();
1404
- const deviceId = analyticsConfig.deviceId || analyticsConfig.device_id || getDefaultDeviceId();
1405
- const eventEndpointUrl = eventEndpoint || process.env.BV_ONE_EVENT_API + '/v1/events';
1406
- const heartbeatEndpointUrl = heartbeatEndpoint || process.env.BV_ONE_EVENT_HEARTBEAT_API + '/v1/events';
1407
-
1408
- if (!resourceType) {
1409
- console.warn('Please check resource_type in your modulesConfig. Otherwise, the player will send default resource_type, ex: RESOURCE_TYPE_VOD_EVENT or RESOURCE_TYPE_LIVE_EVENT !');
1410
- } // @ts-ignore
1411
-
1412
-
1413
- const logTarget = mapLogEvents({
1414
- video,
1415
- playerName: 'shaka',
1416
- version: "2.25.0-canary.24"
1417
- });
1418
- logTarget.all((type, data) => {
1419
- const payload = {
1420
- event_type: 'playback_log',
1421
- // @ts-ignore
1422
- event_name: logEventNames[type] || type,
1423
- event_content: JSON.stringify({
1424
- session_id: sessionId,
1425
- ...data,
1426
- ...getDeviceInfo(),
1427
- ...analyticsConfig,
1428
- resource_id: resourceId,
1429
- resource_type: resourceType || resourceTypes[data.resource_type || 'vod'],
1430
- user_id: userId,
1431
- device_id: deviceId
1432
- }),
1433
- event_time: new Date(data.system_time * 1000).toISOString()
1434
- };
1435
- onPlaylogFired === null || onPlaylogFired === void 0 ? void 0 : onPlaylogFired(payload.event_name, data);
1436
-
1437
- if (!token) {
1438
- return;
1439
- }
1440
-
1441
- retryWithBackoff$1({
1442
- fn: () => axios.post(eventEndpointUrl, payload, {
1443
- headers: {
1444
- 'Kks-Bv-Token': token
1445
- }
1446
- })
1447
- });
1448
- });
1449
- const heartbeatTimer = eventHeartbeat$1({
1450
- token,
1451
- sessionId,
1452
- resourceId,
1453
- userId,
1454
- deviceId,
1455
- endpoint: heartbeatEndpointUrl,
1456
- rawData: {
1457
- analyticsConfig,
1458
- // @ts-ignore
1459
- logTarget
1460
- },
1461
- resourceTypes
1462
- });
1463
- return {
1464
- sendEvent: logTarget.emit,
1465
- sendLog: logTarget.emit,
1466
- reset: () => {
1467
- logTarget.reset();
1468
- clearInterval(heartbeatTimer);
1469
- }
1470
- };
1471
- };
1472
-
1473
- const timeoutError = () => new Error('request timeout');
1474
- /**
1475
- * @param {URL|RequestInfo} url
1476
- * @param {RequestInit} options
1477
- * @param {{responseType: 'json'|'text'}}
1478
- */
1479
-
1480
-
1481
- const retryRequest = (url, options = {}, {
1482
- responseType = 'json',
1483
- timeout = 6,
1484
- retryTimes = 6
1485
- } = {}) => new Promise((resolve, reject) => {
1486
- setTimeout(() => reject(timeoutError()), timeout * 1000);
1487
- fetch(url, options).then(response => {
1488
- var _response$responseTyp;
1489
-
1490
- return resolve(((_response$responseTyp = response[responseType]) === null || _response$responseTyp === void 0 ? void 0 : _response$responseTyp.call(response)) || response);
1491
- }).catch(reject);
1492
- }).catch(error => {
1493
- console.log(error);
1494
-
1495
- if (retryTimes > 0) {
1496
- return retryRequest(url, options, {
1497
- timeout,
1498
- retryTimes: retryTimes - 1
1499
- });
1500
- }
1501
-
1502
- return error;
1503
- });
1504
-
1505
- /* eslint-disable no-param-reassign */
1506
-
1507
- const matchAll = (input, pattern) => {
1508
- const flags = [pattern.global && 'g', pattern.ignoreCase && 'i', pattern.multiline && 'm'].filter(Boolean).join('');
1509
- const clone = new RegExp(pattern, flags);
1510
- return Array.from(function* () {
1511
- let matched = true;
1512
-
1513
- while (1) {
1514
- matched = clone.exec(input);
1515
-
1516
- if (!matched) {
1517
- return;
1518
- }
1519
-
1520
- yield matched;
1521
- }
1522
- }());
1523
- };
1524
-
1525
- const rewriteUrls = (manifest, sourceUrl) => manifest.replace(/((#EXT-X-MEDIA:.*URI=")([^"]*))|((#EXT-X-STREAM-INF.*\n)(.*)(?=\n))/g, (...matches) => [matches[2], matches[5], new URL(matches[3] || matches[6], sourceUrl)].filter(Boolean).join(''));
1526
-
1527
- const filterHlsManifestQualities = (originalManifest, filter) => {
1528
- if (!filter) {
1529
- return;
1530
- }
1531
-
1532
- const manifest = `${originalManifest}\n`;
1533
- const profiles = matchAll(manifest, /RESOLUTION=(\d+)x(\d+)/g).map(([, width, height]) => ({
1534
- width: +width,
1535
- height: +height
1536
- }));
1537
- const allowed = filter(profiles) || profiles;
1538
- const newManifest = manifest.replace(/#EXT-X-STREAM-INF.*RESOLUTION=(\d+)x(\d+).*\n.*\n/g, (item, width, height) => allowed.some(p => p.width === +width && p.height === +height) ? item : '');
1539
- return newManifest !== manifest && newManifest;
1540
- };
1541
-
1542
- const meetRestriction = (quality, {
1543
- minHeight,
1544
- maxHeight
1545
- } = {}) => !(quality.height < minHeight || quality.height > maxHeight);
1546
-
1547
- const selectHlsQualities = async (source, restrictions = {}) => {
1548
- if (!isSafari() || !(restrictions.minHeight || restrictions.maxHeight)) {
1549
- return source;
1550
- }
1551
-
1552
- const selected = getSource(source, {
1553
- preferManifestType: 'hls'
1554
- });
1555
-
1556
- if (!((selected === null || selected === void 0 ? void 0 : selected.type.toLowerCase()) === mimeTypes.hls)) {
1557
- return source;
1558
- }
1559
-
1560
- const filtered = filterHlsManifestQualities(await retryRequest(selected.src, {}, {
1561
- responseType: 'text'
1562
- }), items => items.filter(item => meetRestriction(item, restrictions)));
1563
-
1564
- if (filtered) {
1565
- return { ...selected,
1566
-
1567
- /*
1568
- Native Safari couldn't support blob .m3u8. and will throw MediaError: 4
1569
- We find the hacky method: dataURI.
1570
- By the way, bitmovin also use this form even user gives the blob URI.
1571
- */
1572
- src: `data:application/x-mpegURL,${encodeURI(rewriteUrls(filtered, selected.src))}`
1573
- };
1574
- }
1575
-
1576
- return source;
1577
- };
1578
- // for unit test
1579
-
1580
- /* eslint-disable no-empty */
1581
- const storageKey = 'playcraft-tab-lock';
1582
- const lockRenewTime = 3000;
1583
-
321
+ * This method creates analytics module instance and return it.
322
+ * @param createAnalyticsParameter
323
+ * @returns Analytics module instance
324
+ */
325
+ const createAnalytics = ({ video, onPlaylogFired, modulesConfig }) => {
326
+ const { token, eventEndpoint, heartbeatEndpoint, ...analyticsConfig } = retrieveModuleConfig(modulesConfig, "analytics");
327
+ const resourceId = analyticsConfig.resourceId || analyticsConfig.resource_id || "";
328
+ const resourceType = analyticsConfig.resourceType || analyticsConfig.resource_type || "";
329
+ const userId = analyticsConfig.userId || analyticsConfig.user_id || "";
330
+ const sessionId = analyticsConfig.sessionId || analyticsConfig.session_id || uuidv4_default();
331
+ const deviceId = analyticsConfig.deviceId || analyticsConfig.device_id || getDefaultDeviceId();
332
+ const eventEndpointUrl = eventEndpoint || process.env.BV_ONE_EVENT_API + "/v1/events";
333
+ const heartbeatEndpointUrl = heartbeatEndpoint || process.env.BV_ONE_EVENT_HEARTBEAT_API + "/v1/events";
334
+ if (!resourceType) console.warn("Please check resource_type in your modulesConfig. Otherwise, the player will send default resource_type, ex: RESOURCE_TYPE_VOD_EVENT or RESOURCE_TYPE_LIVE_EVENT !");
335
+ const logTarget = mapLogEvents$1({
336
+ video,
337
+ playerName: "shaka",
338
+ version: process.env.npm_package_version
339
+ });
340
+ logTarget.all((type, data) => {
341
+ const payload = {
342
+ event_type: "playback_log",
343
+ event_name: logEventNames$1[type] || type,
344
+ event_content: JSON.stringify({
345
+ session_id: sessionId,
346
+ ...data,
347
+ ...getDeviceInfo(),
348
+ ...analyticsConfig,
349
+ resource_id: resourceId,
350
+ resource_type: resourceType || resourceTypes[data.resource_type || "vod"],
351
+ user_id: userId,
352
+ device_id: deviceId
353
+ }),
354
+ event_time: (/* @__PURE__ */ new Date(data.system_time * 1e3)).toISOString()
355
+ };
356
+ onPlaylogFired?.(payload.event_name, data);
357
+ if (!token) return;
358
+ retryWithBackoff_default({ fn: () => axios.post(eventEndpointUrl, payload, { headers: { "Kks-Bv-Token": token } }) });
359
+ });
360
+ const heartbeatTimer = heartbeat_default({
361
+ token,
362
+ sessionId,
363
+ resourceId,
364
+ userId,
365
+ deviceId,
366
+ endpoint: heartbeatEndpointUrl,
367
+ rawData: {
368
+ analyticsConfig,
369
+ logTarget
370
+ },
371
+ resourceTypes
372
+ });
373
+ return {
374
+ sendEvent: logTarget.emit,
375
+ sendLog: logTarget.emit,
376
+ reset: () => {
377
+ logTarget.reset();
378
+ clearInterval(heartbeatTimer);
379
+ }
380
+ };
381
+ };
382
+
383
+ //#endregion
384
+ //#region src/util/ensureTabLock.js
385
+ const storageKey = "playcraft-tab-lock";
386
+ const lockRenewTime = 3e3;
1584
387
  const ensureTabLock = () => {
1585
- let saved = {};
1586
-
1587
- try {
1588
- saved = JSON.parse(localStorage[storageKey]);
1589
- } catch (e) {
1590
- console.log('Can read saved data for tab lock.', e);
1591
- }
1592
-
1593
- const {
1594
- expireTime
1595
- } = saved;
1596
-
1597
- if (Date.now() <= expireTime) {
1598
- return;
1599
- }
1600
-
1601
- const id = uuidv4();
1602
-
1603
- const renewLock = () => {
1604
- localStorage[storageKey] = JSON.stringify({
1605
- id,
1606
- expireTime: Date.now() + lockRenewTime * 3
1607
- });
1608
- };
1609
-
1610
- const renewInterval = setInterval(renewLock, lockRenewTime);
1611
-
1612
- const releaseLock = () => {
1613
- clearInterval(renewInterval);
1614
- window.removeEventListener('beforeunload', releaseLock);
1615
- window.removeEventListener('unload', releaseLock);
1616
- localStorage[storageKey] = {
1617
- expireTime: Date.now() - 1
1618
- };
1619
- };
1620
-
1621
- window.addEventListener('beforeunload', releaseLock);
1622
- window.addEventListener('unload', releaseLock);
1623
- return releaseLock;
1624
- };
1625
-
1626
- let lastError = '';
388
+ let saved = {};
389
+ try {
390
+ saved = JSON.parse(localStorage[storageKey]);
391
+ } catch (e) {
392
+ console.log("Can read saved data for tab lock.", e);
393
+ }
394
+ const { expireTime } = saved;
395
+ if (Date.now() <= expireTime) return;
396
+ const id = uuidv4_default();
397
+ const renewLock = () => {
398
+ localStorage[storageKey] = JSON.stringify({
399
+ id,
400
+ expireTime: Date.now() + lockRenewTime * 3
401
+ });
402
+ };
403
+ const renewInterval = setInterval(renewLock, lockRenewTime);
404
+ const releaseLock = () => {
405
+ clearInterval(renewInterval);
406
+ window.removeEventListener("beforeunload", releaseLock);
407
+ window.removeEventListener("unload", releaseLock);
408
+ localStorage[storageKey] = { expireTime: Date.now() - 1 };
409
+ };
410
+ window.addEventListener("beforeunload", releaseLock);
411
+ window.addEventListener("unload", releaseLock);
412
+ return releaseLock;
413
+ };
414
+ var ensureTabLock_default = ensureTabLock;
415
+
416
+ //#endregion
417
+ //#region src/util/addSentry.js
418
+ let lastError = "";
1627
419
  const defaultOptions = {
1628
- ignoreErrors: ['AbortError: The play() request was interrupted', 'i.context.logger'],
1629
- beforeSend: event => {
1630
- if (lastError.message === event.exception.values[0].value && Date.now() - lastError.date < 10000) {
1631
- lastError.date = Date.now();
1632
- return null;
1633
- }
1634
-
1635
- lastError = {
1636
- date: Date.now(),
1637
- message: event.exception.values[0].value
1638
- };
1639
- return event;
1640
- }
1641
- };
1642
-
1643
- const addSentry = ({
1644
- key,
1645
- ...options
1646
- }) => {
1647
- const script = document.createElement('script');
1648
- script.crossorigin = 'anonymous';
1649
- script.src = `https://js.sentry-cdn.com/${key}.min.js`;
1650
- script.addEventListener('load', () => {
1651
- window.Sentry.onLoad(() => {
1652
- window.Sentry.init({ ...defaultOptions,
1653
- ...options
1654
- });
1655
- });
1656
- }, {
1657
- once: true
1658
- });
1659
- document.body.append(script);
1660
- };
1661
-
420
+ ignoreErrors: ["AbortError: The play() request was interrupted", "i.context.logger"],
421
+ beforeSend: (event) => {
422
+ if (lastError.message === event.exception.values[0].value && Date.now() - lastError.date < 1e4) {
423
+ lastError.date = Date.now();
424
+ return null;
425
+ }
426
+ lastError = {
427
+ date: Date.now(),
428
+ message: event.exception.values[0].value
429
+ };
430
+ return event;
431
+ }
432
+ };
433
+ const addSentry = ({ key, ...options }) => {
434
+ const script = document.createElement("script");
435
+ script.crossorigin = "anonymous";
436
+ script.src = `https://js.sentry-cdn.com/${key}.min.js`;
437
+ script.addEventListener("load", () => {
438
+ window.Sentry.onLoad(() => {
439
+ window.Sentry.init({
440
+ ...defaultOptions,
441
+ ...options
442
+ });
443
+ });
444
+ }, { once: true });
445
+ document.body.append(script);
446
+ };
447
+ var addSentry_default = addSentry;
448
+
449
+ //#endregion
450
+ //#region src/util/handleIOSHeadphonesDisconnection.js
1662
451
  /**
1663
- * @description Unplugging / disconnecting headphones will pause video in iOS,
1664
- * and in some iOS versions, video is paused without firing a pause event.
1665
- * Pause the video if paused by iOS in this case.
1666
- * @param {HTMLMediaElement} video
1667
- */
1668
-
1669
- const handleIOSHeadphonesDisconnection = ({
1670
- maxStuckSeconds = 1
1671
- } = {}) => {
1672
- const video = document.querySelector('video');
1673
-
1674
- if (video && getOS().name === 'iOS') {
1675
- let playState = {
1676
- playing: false
1677
- };
1678
-
1679
- const saveState = ({
1680
- playing = playState.playing
1681
- } = {}) => {
1682
- playState = {
1683
- playing,
1684
- ...(playing && {
1685
- lastTimeUpdate: Date.now()
1686
- })
1687
- };
1688
- };
1689
-
1690
- video.addEventListener('pause', () => {
1691
- playState = {
1692
- playing: false
1693
- };
1694
- });
1695
- video.addEventListener('seeking', () => {
1696
- playState = {
1697
- playing: false
1698
- };
1699
- });
1700
- video.addEventListener('waiting', () => {
1701
- playState = {
1702
- playing: false
1703
- };
1704
- });
1705
- video.addEventListener('webkitpresentationmodechanged', () => {
1706
- playState = {
1707
- playing: false,
1708
- pauseDetection: Date.now() + 5000
1709
- };
1710
- });
1711
- video.addEventListener('timeupdate', () => {
1712
- if (!(video.currentTime >= 0 && video.currentTime <= Date.now())) {
1713
- return;
1714
- }
1715
-
1716
- if (!video.paused) {
1717
- const delta = Date.now() - playState.lastTimeUpdate;
1718
- playState.lastTimeUpdate = Date.now();
1719
-
1720
- if (delta > 0 && delta < 1000) {
1721
- playState.playing = true;
1722
- }
1723
- }
1724
-
1725
- saveState({
1726
- playing: !video.paused
1727
- });
1728
- });
1729
- video.addEventListener('ratechange', saveState);
1730
- setInterval(() => {
1731
- if (video.paused || !playState.playing || playState.pauseDetection >= Date.now()) {
1732
- return;
1733
- }
1734
-
1735
- const secondsStuck = (Date.now() - playState.lastTimeUpdate) / 1000;
1736
-
1737
- if (secondsStuck >= maxStuckSeconds) {
1738
- console.log('Video is not playing, pause to workaround iOS unpluging headphones');
1739
- video.pause();
1740
- }
1741
- }, 200);
1742
- }
1743
- };
1744
-
1745
- const ewma = halfLife => {
1746
- let alpha = Math.exp(Math.log(0.5) / halfLife);
1747
- let estimate = 0;
1748
- let totalWeight = 0;
1749
- return {
1750
- updateAlpha: newHalfLife => {
1751
- alpha = Math.exp(Math.log(0.5) / newHalfLife);
1752
- },
1753
- sample: (weight, value) => {
1754
- const adjAlpha = alpha ** weight;
1755
- const newEstimate = value * (1 - adjAlpha) + adjAlpha * estimate;
1756
-
1757
- if (!Number.isNaN(newEstimate)) {
1758
- estimate = newEstimate;
1759
- totalWeight += weight;
1760
- }
1761
- },
1762
- getEstimate: () => {
1763
- const zeroFactor = 1 - alpha ** totalWeight;
1764
- return estimate / zeroFactor;
1765
- }
1766
- };
1767
- };
1768
-
1769
- /* eslint-disable no-param-reassign */
1770
-
452
+ * @description Unplugging / disconnecting headphones will pause video in iOS,
453
+ * and in some iOS versions, video is paused without firing a pause event.
454
+ * Pause the video if paused by iOS in this case.
455
+ * @param {HTMLMediaElement} video
456
+ */
457
+ const handleIOSHeadphonesDisconnection = ({ maxStuckSeconds = 1 } = {}) => {
458
+ const video = document.querySelector("video");
459
+ if (video && getOS().name === "iOS") {
460
+ let playState = { playing: false };
461
+ const saveState = ({ playing = playState.playing } = {}) => {
462
+ playState = {
463
+ playing,
464
+ ...playing && { lastTimeUpdate: Date.now() }
465
+ };
466
+ };
467
+ video.addEventListener("pause", () => {
468
+ playState = { playing: false };
469
+ });
470
+ video.addEventListener("seeking", () => {
471
+ playState = { playing: false };
472
+ });
473
+ video.addEventListener("waiting", () => {
474
+ playState = { playing: false };
475
+ });
476
+ video.addEventListener("webkitpresentationmodechanged", () => {
477
+ playState = {
478
+ playing: false,
479
+ pauseDetection: Date.now() + 5e3
480
+ };
481
+ });
482
+ video.addEventListener("timeupdate", () => {
483
+ if (!(video.currentTime >= 0 && video.currentTime <= Date.now())) return;
484
+ if (!video.paused) {
485
+ const delta = Date.now() - playState.lastTimeUpdate;
486
+ playState.lastTimeUpdate = Date.now();
487
+ if (delta > 0 && delta < 1e3) playState.playing = true;
488
+ }
489
+ saveState({ playing: !video.paused });
490
+ });
491
+ video.addEventListener("ratechange", saveState);
492
+ setInterval(() => {
493
+ if (video.paused || !playState.playing || playState.pauseDetection >= Date.now()) return;
494
+ if ((Date.now() - playState.lastTimeUpdate) / 1e3 >= maxStuckSeconds) {
495
+ console.log("Video is not playing, pause to workaround iOS unpluging headphones");
496
+ video.pause();
497
+ }
498
+ }, 200);
499
+ }
500
+ };
501
+ var handleIOSHeadphonesDisconnection_default = handleIOSHeadphonesDisconnection;
502
+
503
+ //#endregion
504
+ //#region src/util/ewma.js
505
+ const ewma = (halfLife) => {
506
+ let alpha = Math.exp(Math.log(.5) / halfLife);
507
+ let estimate = 0;
508
+ let totalWeight = 0;
509
+ return {
510
+ updateAlpha: (newHalfLife) => {
511
+ alpha = Math.exp(Math.log(.5) / newHalfLife);
512
+ },
513
+ sample: (weight, value) => {
514
+ const adjAlpha = alpha ** weight;
515
+ const newEstimate = value * (1 - adjAlpha) + adjAlpha * estimate;
516
+ if (!Number.isNaN(newEstimate)) {
517
+ estimate = newEstimate;
518
+ totalWeight += weight;
519
+ }
520
+ },
521
+ getEstimate: () => {
522
+ const zeroFactor = 1 - alpha ** totalWeight;
523
+ return estimate / zeroFactor;
524
+ }
525
+ };
526
+ };
527
+ var ewma_default = ewma;
528
+
529
+ //#endregion
530
+ //#region src/playerCore/latencyManager.js
1771
531
  const getBufferedAhead = (mediaSource, video) => {
1772
- const items = needNativeHls() ? [video.buffered] : Array.from(mediaSource.sourceBuffers, item => item.buffered);
1773
-
1774
- if (items.length < 1) {
1775
- return 0;
1776
- }
1777
-
1778
- return Math.min(...items.map(buffered => Math.max(video.currentTime, ...Array.from({
1779
- length: (buffered === null || buffered === void 0 ? void 0 : buffered.length) || 0
1780
- }, (_, i) => buffered.end(i))))) - video.currentTime;
532
+ const items = needNativeHls() ? [video.buffered] : Array.from(mediaSource.sourceBuffers, (item) => item.buffered);
533
+ if (items.length < 1) return 0;
534
+ return Math.min(...items.map((buffered) => Math.max(video.currentTime, ...Array.from({ length: buffered?.length || 0 }, (_, i) => buffered.end(i))))) - video.currentTime;
1781
535
  };
1782
-
1783
536
  const constants = {
1784
- hlsHighBufferFactor: 1.4,
1785
- dashHighBufferFactor: 1.2
1786
- };
1787
-
1788
- const getInfo = ({
1789
- video,
1790
- player,
1791
- bufferLength
1792
- }) => {
1793
- var _player$getPresentati;
1794
-
1795
- return {
1796
- systemTime: Date.now(),
1797
- playbackTime: video.currentTime + ((player === null || player === void 0 ? void 0 : (_player$getPresentati = player.getPresentationStartTimeAsDate()) === null || _player$getPresentati === void 0 ? void 0 : _player$getPresentati.getTime()) || 0),
1798
- playbackRate: video.playbackRate,
1799
- bufferedAhead: getBufferedAhead(player.mediaSource, video),
1800
- downloadSpeed: bufferLength.getEstimate()
1801
- };
1802
- };
1803
-
1804
- const manageLatencySafariNative = ({
1805
- player,
1806
- video,
1807
- bufferLength
1808
- }, options) => {
1809
- let lastBuffered;
1810
- let remainingAttempts = 2; // TODO assume no handle start-over
1811
-
1812
- const updateIntervalId = setInterval(() => {
1813
- var _options$onUpdate;
1814
-
1815
- const info = getInfo({
1816
- player,
1817
- video,
1818
- bufferLength
1819
- });
1820
- bufferLength.sample(1, Math.max(0, info.bufferedAhead));
1821
- (_options$onUpdate = options.onUpdate) === null || _options$onUpdate === void 0 ? void 0 : _options$onUpdate.call(options, { ...options,
1822
- ...info
1823
- }); // Magic numbers to be tuned further
1824
-
1825
- if (video.paused) {
1826
- remainingAttempts = 2;
1827
- } else if (info.bufferedAhead > 8) {
1828
- console.debug(`Jump`, info.bufferedAhead, lastBuffered);
1829
- video.currentTime += info.bufferedAhead;
1830
- } else if (remainingAttempts > 0 && // by default target buffer length * factor is 2.1
1831
- info.bufferedAhead > options.targetBufferLength * constants.hlsHighBufferFactor && // Safari updates seek range when a segment is completely added
1832
- info.bufferedAhead > lastBuffered + options.hlsSegmentLength / 4) {
1833
- console.debug(`Seek ahead`, info.bufferedAhead, lastBuffered);
1834
- video.currentTime += 10;
1835
- remainingAttempts -= 1;
1836
- }
1837
-
1838
- if (bufferLength.getEstimate() > options.targetBufferLength * constants.hlsHighBufferFactor) {
1839
- remainingAttempts = 2;
1840
- }
1841
-
1842
- lastBuffered = info.bufferedAhead;
1843
- }, options.updateInterval);
1844
- return () => clearInterval(updateIntervalId);
1845
- };
1846
-
1847
- const manageLatencyMse = ({
1848
- player,
1849
- video,
1850
- bufferLength
1851
- }, options) => {
1852
- let lastPlaybackTime = 0;
1853
- player.configure({
1854
- manifest: {
1855
- dash: {
1856
- ignoreSuggestedPresentationDelay: true
1857
- }
1858
- },
1859
- streaming: {
1860
- lowLatencyMode: true,
1861
- bufferingGoal: 10.0,
1862
- rebufferingGoal: 0.01,
1863
- updateIntervalSeconds: 0.2,
1864
- gapDetectionThreshold: 0.001,
1865
- inaccurateManifestTolerance: 1,
1866
- retryParameters: {
1867
- baseDelay: 50,
1868
- backoffFactor: 1.2,
1869
- fuzzFactor: 0,
1870
- maxAttempts: 66
1871
- }
1872
- }
1873
- });
1874
- const updateIntervalId = setInterval(() => {
1875
- var _options$onUpdate2;
1876
-
1877
- const info = getInfo({
1878
- player,
1879
- video,
1880
- bufferLength
1881
- });
1882
- bufferLength.sample(1, Math.max(0, info.bufferedAhead));
1883
- (_options$onUpdate2 = options.onUpdate) === null || _options$onUpdate2 === void 0 ? void 0 : _options$onUpdate2.call(options, { ...options,
1884
- ...info
1885
- });
1886
-
1887
- if (!player || !video || video.currentTime < lastPlaybackTime + 0.08) {
1888
- return;
1889
- }
1890
-
1891
- lastPlaybackTime = video.currentTime;
1892
-
1893
- if (info.bufferedAhead > 5) {
1894
- video.currentTime = video.currentTime + info.bufferedAhead - 2.5;
1895
- console.debug(`Buffer abundant, seek from ${video.currentTime} to ${video.currentTime}`);
1896
- return;
1897
- } // start speedup at 120%, stop when under 100%, to avoid frequent speed changes when near 100%
1898
-
1899
-
1900
- const bufferCap = +options.targetBufferLength * (info.playbackRate > 1 ? 1 : constants.dashHighBufferFactor);
1901
- const rate = bufferLength.getEstimate() > bufferCap && options.speedup ? +options.speedup : 0;
1902
-
1903
- if ((video === null || video === void 0 ? void 0 : video.currentTime) > 0) {
1904
- video.playbackRate = 1 + rate / 100;
1905
- }
1906
- }, options.updateInterval);
1907
- return () => clearInterval(updateIntervalId);
1908
- };
1909
-
1910
- const latencyManager = (player, video) => {
1911
- const currentOptions = {
1912
- speedup: 7,
1913
- targetBufferLength: 1.5,
1914
- hlsSegmentLength: 4,
1915
- updateInterval: 100
1916
- };
1917
- let stop; // TODO reset on updateInterval change
1918
-
1919
- const bufferLength = ewma(1500 / currentOptions.updateInterval);
1920
-
1921
- const configure = config => {
1922
- if (!config) {
1923
- return getInfo({
1924
- player,
1925
- video,
1926
- bufferLength
1927
- });
1928
- }
1929
-
1930
- if ('enabled' in config && Boolean(config.enabled) !== currentOptions.enabled) {
1931
- var _stop;
1932
-
1933
- (_stop = stop) === null || _stop === void 0 ? void 0 : _stop();
1934
-
1935
- if (config.enabled) {
1936
- stop = (needNativeHls() ? manageLatencySafariNative : manageLatencyMse)({
1937
- player,
1938
- video,
1939
- bufferLength
1940
- }, currentOptions);
1941
- }
1942
- }
1943
-
1944
- Object.assign(currentOptions, config);
1945
- if ('segmentTimestampOffset' in config) window.segmentTimestampOffset = config.segmentTimestampOffset;
1946
- };
1947
-
1948
- return {
1949
- configure
1950
- };
1951
- };
1952
-
1953
- /**
1954
- * Parses an XML duration string.
1955
- * Negative values are not supported. Years and months are treated as exactly
1956
- * 365 and 30 days respectively.
1957
- * @param {string} durationString The duration string, e.g., "PT1H3M43.2S",
1958
- * which means 1 hour, 3 minutes, and 43.2 seconds.
1959
- * @return {?number} The parsed duration in seconds on success; otherwise,
1960
- * return null.
1961
- * @see {@link http://www.datypic.com/sc/xsd/t-xsd_duration.html}
1962
- */
1963
- const parseDuration = durationString => {
1964
- if (!durationString) {
1965
- return null;
1966
- }
1967
-
1968
- const re = '^P(?:([0-9]*)Y)?(?:([0-9]*)M)?(?:([0-9]*)D)?' + '(?:T(?:([0-9]*)H)?(?:([0-9]*)M)?(?:([0-9.]*)S)?)?$';
1969
- const matches = new RegExp(re).exec(durationString);
1970
-
1971
- if (!matches) {
1972
- console.warning('Invalid duration string:', durationString);
1973
- return null;
1974
- } // Note: Number(null) == 0 but Number(undefined) == NaN.
1975
-
1976
-
1977
- const years = Number(matches[1] || null);
1978
- const months = Number(matches[2] || null);
1979
- const days = Number(matches[3] || null);
1980
- const hours = Number(matches[4] || null);
1981
- const minutes = Number(matches[5] || null);
1982
- const seconds = Number(matches[6] || null); // Assume a year always has 365 days and a month always has 30 days.
1983
-
1984
- const d = 60 * 60 * 24 * 365 * years + 60 * 60 * 24 * 30 * months + 60 * 60 * 24 * days + 60 * 60 * hours + 60 * minutes + seconds;
1985
- return Number.isFinite(d) ? d : null;
1986
- };
1987
-
1988
- const normalize = (doc, template, period) => {
1989
- const segmentDuration = parseFloat(template.getAttribute('duration'), 10);
1990
-
1991
- if (!segmentDuration) {
1992
- return;
1993
- }
1994
-
1995
- const timescale = parseFloat(template.getAttribute('timescale'), 10);
1996
- const periodDuration = parseDuration(period.getAttribute('duration'));
1997
- const item = doc.createElement('S');
1998
- item.setAttribute('d', segmentDuration);
1999
- item.setAttribute('r', Math.ceil(periodDuration * timescale / segmentDuration) - 1);
2000
- const timeline = doc.createElement('SegmentTimeline');
2001
- timeline.appendChild(item);
2002
- template.appendChild(timeline);
2003
- template.removeAttribute('duration');
2004
- };
2005
-
2006
- const fixDashManifest = (data, {
2007
- minTimeShiftBufferDepth
2008
- } = {}) => {
2009
- const doc = new DOMParser().parseFromString(data, 'text/xml'); // only dynamic manifest needs timeShiftBufferDepth
2010
-
2011
- const root = doc.children[0];
2012
-
2013
- if (root.getAttribute('type') === 'dynamic') {
2014
- if (root.getAttribute('timeShiftBufferDepth') < minTimeShiftBufferDepth) {
2015
- root.setAttribute('timeShiftBufferDepth', minTimeShiftBufferDepth);
2016
- }
2017
- } else if (root.hasAttribute('timeShiftBufferDepth')) {
2018
- root.removeAttribute('timeShiftBufferDepth');
2019
- } // workaround multi-period segment template bug, normalize to segment timelines
2020
-
2021
-
2022
- if (doc.querySelectorAll('Period').length > 1) {
2023
- Array.from(doc.querySelectorAll('Period')).forEach(period => {
2024
- Array.from(period.querySelectorAll('SegmentTemplate')).forEach(template => normalize(doc, template, period));
2025
- });
2026
- }
2027
-
2028
- window.manifestDoc = doc;
2029
- return new XMLSerializer().serializeToString(doc);
2030
- };
2031
-
2032
- const loadScript = url => new Promise(resolve => {
2033
- const script = Object.assign(document.createElement('script'), {
2034
- async: true,
2035
- src: url
2036
- });
2037
- script.addEventListener('load', resolve);
2038
- document.body.appendChild(script);
2039
- });
2040
-
2041
- /* eslint-disable no-empty */
2042
- const SENDER_URL = 'https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1';
2043
-
2044
- const getContext = () => {
2045
- var _cast$framework;
2046
-
2047
- return window.cast && ((_cast$framework = cast.framework) === null || _cast$framework === void 0 ? void 0 : _cast$framework.CastContext.getInstance());
2048
- };
2049
- /* global chrome, cast */
2050
-
2051
-
2052
- const ensureSenderFramework = () => {
2053
- if (window.cast && cast.framework && window.chrome && chrome.cast) {
2054
- return Promise.resolve(getContext());
2055
- }
2056
-
2057
- if (!window.loadSenderFramework) {
2058
- window.loadSenderFramework = new Promise((resolve, reject) => {
2059
- // eslint-disable-next-line no-underscore-dangle
2060
- window.__onGCastApiAvailable = isAvailable => {
2061
- if (isAvailable) {
2062
- resolve(getContext());
2063
- } else {
2064
- reject();
2065
- }
2066
- };
2067
-
2068
- loadScript(SENDER_URL);
2069
- });
2070
- }
2071
-
2072
- return window.loadSenderFramework;
2073
- };
2074
-
2075
- const initSender = async ({
2076
- appId,
2077
- ...options
2078
- }) => {
2079
- const context = await ensureSenderFramework();
2080
- context.setOptions({
2081
- receiverApplicationId: appId,
2082
- resumeSavedSession: true,
2083
- autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED,
2084
- ...options
2085
- });
2086
- };
2087
-
2088
- const subscribeCastState = handleStateChange => {
2089
- const waitFramework = ensureSenderFramework().then(() => {
2090
- const name = cast.framework.CastContextEventType.CAST_STATE_CHANGED;
2091
- const context = getContext();
2092
-
2093
- const onChange = ({
2094
- castState
2095
- }) => handleStateChange(castState);
2096
-
2097
- handleStateChange(context.getCastState());
2098
- context.addEventListener(name, onChange);
2099
- return () => context.removeEventListener(name, onChange);
2100
- });
2101
- return () => waitFramework.then(removeListeners => removeListeners());
2102
- };
2103
-
2104
- const disconnect = async () => {
2105
- await ensureSenderFramework();
2106
-
2107
- try {
2108
- getContext().endCurrentSession(true);
2109
- } catch (e) {
2110
- // eslint-disable-next-line no-console
2111
- console.log(e);
2112
- }
2113
- };
2114
-
2115
- const loadMedia = ({
2116
- source,
2117
- contentType,
2118
- contentId: paramContentId,
2119
- sourceType,
2120
- apiConfig,
2121
- customData
2122
- }) => {
2123
- var _sourceInfo$drm, _sourceInfo$drm2, _sourceInfo$drm2$wide, _session$getMediaSess, _currentMedia$customD;
2124
-
2125
- const session = getContext().getCurrentSession();
2126
-
2127
- if (!session) {
2128
- return 'receiver_unavailable';
2129
- } // not connected actually, but there is no error code for the situation
2130
-
2131
-
2132
- const sourceInfo = source && getSource(source);
2133
- const contentId = paramContentId || (sourceInfo === null || sourceInfo === void 0 ? void 0 : sourceInfo.src);
2134
- const drm = (sourceInfo === null || sourceInfo === void 0 ? void 0 : sourceInfo.drm) && { ...((_sourceInfo$drm = sourceInfo.drm) === null || _sourceInfo$drm === void 0 ? void 0 : _sourceInfo$drm.widevine),
2135
- licenseUrl: (_sourceInfo$drm2 = sourceInfo.drm) === null || _sourceInfo$drm2 === void 0 ? void 0 : (_sourceInfo$drm2$wide = _sourceInfo$drm2.widevine) === null || _sourceInfo$drm2$wide === void 0 ? void 0 : _sourceInfo$drm2$wide.licenseUri
2136
- };
2137
- const request = new chrome.cast.media.LoadRequest(Object.assign(new chrome.cast.media.MediaInfo(contentId), {
2138
- contentId,
2139
- contentID: contentId,
2140
- customData: {
2141
- itemType: contentType,
2142
- ...apiConfig,
2143
- customHeaders: apiConfig === null || apiConfig === void 0 ? void 0 : apiConfig.headers,
2144
- customQuery: apiConfig === null || apiConfig === void 0 ? void 0 : apiConfig.params,
2145
- mediaSource: sourceType,
2146
- ...(drm && {
2147
- drm
2148
- }),
2149
- ...customData
2150
- },
2151
- metadata: new chrome.cast.media.GenericMediaMetadata()
2152
- })); // TODO playback start time: request.currentTime
2153
-
2154
- const currentMedia = (session === null || session === void 0 ? void 0 : (_session$getMediaSess = session.getMediaSession()) === null || _session$getMediaSess === void 0 ? void 0 : _session$getMediaSess.media) || {}; // no need to load if already playing TODO extrack checking
2155
-
2156
- if (contentId === currentMedia.contentId && contentType === ((_currentMedia$customD = currentMedia.customData) === null || _currentMedia$customD === void 0 ? void 0 : _currentMedia$customD.itemType)) {
2157
- return Promise.resolve();
2158
- }
2159
-
2160
- return session.loadMedia(request).catch(e => console.log(e));
2161
- }; // options: {contentId, customData: {...}}
2162
-
2163
-
2164
- const linkCast = ({
2165
- onChange,
2166
- ...options
2167
- }) => subscribeCastState(state => {
2168
- if (state === 'CONNECTED') {
2169
- loadMedia(options); // TODO refactor the interface
2170
- }
2171
-
2172
- onChange === null || onChange === void 0 ? void 0 : onChange(state);
537
+ hlsHighBufferFactor: 1.4,
538
+ dashHighBufferFactor: 1.2
539
+ };
540
+ const getInfo = ({ video, player, bufferLength }) => ({
541
+ systemTime: Date.now(),
542
+ playbackTime: video.currentTime + (player?.getPresentationStartTimeAsDate()?.getTime() || 0),
543
+ playbackRate: video.playbackRate,
544
+ bufferedAhead: getBufferedAhead(player.mediaSource, video),
545
+ downloadSpeed: bufferLength.getEstimate()
2173
546
  });
2174
-
2175
- const getChapterIndex = (chapters, time) => chapters.reduce((currentIndex, chapter, index) => time >= chapter.startTime && chapter.startTime > chapters[currentIndex].startTime ? index : currentIndex, 0);
2176
-
2177
- const dispatchChapterEvents = ({
2178
- media,
2179
- chapters = [],
2180
- getTime
2181
- }) => {
2182
- const state = {};
2183
-
2184
- const updateChapter = event => {
2185
- if (media.webkitDisplayingFullscreen) {
2186
- return; // in iOS fullscreen UI updates are not possible, so skip
2187
- }
2188
-
2189
- const next = getChapterIndex(chapters, getTime());
2190
-
2191
- if (next !== state.currentChapterIndex) {
2192
- media.dispatchEvent(Object.assign(new CustomEvent('chapterChange'), {
2193
- action: /seek/.test(event === null || event === void 0 ? void 0 : event.type) ? 'seek' : '',
2194
- chapterIndex: next,
2195
- chapter: chapters[next]
2196
- }));
2197
- }
2198
-
2199
- state.currentChapterIndex = next;
2200
- };
2201
-
2202
- const listeners = [on(media, 'seeking', updateChapter), on(media, 'timeupdate', updateChapter), on(media, 'seeked', updateChapter), on(media, 'webkitendfullscreen', updateChapter)];
2203
- const checkInterval = setInterval(() => {
2204
- var _chapters;
2205
-
2206
- const timeToNext = (((_chapters = chapters[state.currentChapterIndex + 1]) === null || _chapters === void 0 ? void 0 : _chapters.startTime) - getTime() + 0.05) / media.playbackRate;
2207
-
2208
- if (timeToNext >= 0 && timeToNext <= 1) {
2209
- state.timerId = setTimeout(updateChapter, timeToNext * 1000);
2210
- }
2211
- }, 1000);
2212
- return () => {
2213
- clearInterval(checkInterval);
2214
- clearInterval(state.timerId);
2215
- listeners.forEach(removeListener => removeListener());
2216
- };
547
+ const manageLatencySafariNative = ({ player, video, bufferLength }, options) => {
548
+ let lastBuffered;
549
+ let remainingAttempts = 2;
550
+ const updateIntervalId = setInterval(() => {
551
+ const info = getInfo({
552
+ player,
553
+ video,
554
+ bufferLength
555
+ });
556
+ bufferLength.sample(1, Math.max(0, info.bufferedAhead));
557
+ options.onUpdate?.({
558
+ ...options,
559
+ ...info
560
+ });
561
+ if (video.paused) remainingAttempts = 2;
562
+ else if (info.bufferedAhead > 8) {
563
+ console.debug(`Jump`, info.bufferedAhead, lastBuffered);
564
+ video.currentTime += info.bufferedAhead;
565
+ } else if (remainingAttempts > 0 && info.bufferedAhead > options.targetBufferLength * constants.hlsHighBufferFactor && info.bufferedAhead > lastBuffered + options.hlsSegmentLength / 4) {
566
+ console.debug(`Seek ahead`, info.bufferedAhead, lastBuffered);
567
+ video.currentTime += 10;
568
+ remainingAttempts -= 1;
569
+ }
570
+ if (bufferLength.getEstimate() > options.targetBufferLength * constants.hlsHighBufferFactor) remainingAttempts = 2;
571
+ lastBuffered = info.bufferedAhead;
572
+ }, options.updateInterval);
573
+ return () => clearInterval(updateIntervalId);
574
+ };
575
+ const manageLatencyMse = ({ player, video, bufferLength }, options) => {
576
+ let lastPlaybackTime = 0;
577
+ player.configure({
578
+ manifest: { dash: { ignoreSuggestedPresentationDelay: true } },
579
+ streaming: {
580
+ lowLatencyMode: true,
581
+ bufferingGoal: 10,
582
+ rebufferingGoal: .01,
583
+ updateIntervalSeconds: .2,
584
+ gapDetectionThreshold: .001,
585
+ inaccurateManifestTolerance: 1,
586
+ retryParameters: {
587
+ baseDelay: 50,
588
+ backoffFactor: 1.2,
589
+ fuzzFactor: 0,
590
+ maxAttempts: 66
591
+ }
592
+ }
593
+ });
594
+ const updateIntervalId = setInterval(() => {
595
+ const info = getInfo({
596
+ player,
597
+ video,
598
+ bufferLength
599
+ });
600
+ bufferLength.sample(1, Math.max(0, info.bufferedAhead));
601
+ options.onUpdate?.({
602
+ ...options,
603
+ ...info
604
+ });
605
+ if (!player || !video || video.currentTime < lastPlaybackTime + .08) return;
606
+ lastPlaybackTime = video.currentTime;
607
+ if (info.bufferedAhead > 5) {
608
+ video.currentTime = video.currentTime + info.bufferedAhead - 2.5;
609
+ console.debug(`Buffer abundant, seek from ${video.currentTime} to ${video.currentTime}`);
610
+ return;
611
+ }
612
+ const bufferCap = +options.targetBufferLength * (info.playbackRate > 1 ? 1 : constants.dashHighBufferFactor);
613
+ const rate = bufferLength.getEstimate() > bufferCap && options.speedup ? +options.speedup : 0;
614
+ if (video?.currentTime > 0) video.playbackRate = 1 + rate / 100;
615
+ }, options.updateInterval);
616
+ return () => clearInterval(updateIntervalId);
2217
617
  };
2218
-
2219
- export { addSentry, loadMedia as castMedia, createAnalytics, createApi, dispatchChapterEvents, ensureTabLock, fixDashManifest, getContentInfo, getStreamInfo, getVersion, handleIOSHeadphonesDisconnection, initSender, latencyManager, linkCast, logEventNames$1 as logEventNames, mapLogEvents$1 as mapLogEvents, playlogv3, selectHlsQualities, startPlaybackSession as startSession, disconnect as stopCast, subscribeCastState, validateEnvironment };
618
+ const latencyManager = (player, video) => {
619
+ const currentOptions = {
620
+ speedup: 7,
621
+ targetBufferLength: 1.5,
622
+ hlsSegmentLength: 4,
623
+ updateInterval: 100
624
+ };
625
+ let stop;
626
+ const bufferLength = ewma_default(1500 / currentOptions.updateInterval);
627
+ const configure = (config) => {
628
+ if (!config) return getInfo({
629
+ player,
630
+ video,
631
+ bufferLength
632
+ });
633
+ if ("enabled" in config && Boolean(config.enabled) !== currentOptions.enabled) {
634
+ stop?.();
635
+ if (config.enabled) stop = (needNativeHls() ? manageLatencySafariNative : manageLatencyMse)({
636
+ player,
637
+ video,
638
+ bufferLength
639
+ }, currentOptions);
640
+ }
641
+ Object.assign(currentOptions, config);
642
+ if ("segmentTimestampOffset" in config) window.segmentTimestampOffset = config.segmentTimestampOffset;
643
+ };
644
+ return { configure };
645
+ };
646
+ var latencyManager_default = latencyManager;
647
+
648
+ //#endregion
649
+ export { addSentry_default as addSentry, loadMedia as castMedia, createAnalytics, createApi, dispatchChapterEvents, ensureTabLock_default as ensureTabLock, fixDashManifest_default as fixDashManifest, getContentInfo, getStreamInfo, getVersion, handleIOSHeadphonesDisconnection_default as handleIOSHeadphonesDisconnection, initSender, latencyManager_default as latencyManager, linkCast, logEventNames, mapLogEvents, playlogv3_exports as playlogv3, selectHlsQualities, startSession_default as startSession, disconnect as stopCast, subscribeCastState, validateEnvironment };