eb-player 2.2.0 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3341,6 +3341,26 @@
3341
3341
  flex-direction: row-reverse;
3342
3342
  }
3343
3343
 
3344
+ [dir="rtl"] .eb-seekbar-buffered,
3345
+ [dir="rtl"] .eb-seekbar-progress {
3346
+ left: auto;
3347
+ right: 0;
3348
+ }
3349
+
3350
+ [dir="rtl"] .eb-seekbar-thumb {
3351
+ right: auto;
3352
+ left: -6px;
3353
+ }
3354
+
3355
+ [dir="rtl"] .eb-chapter-marker {
3356
+ transform: translateX(50%);
3357
+ }
3358
+
3359
+ [dir="rtl"] .eb-epg-segment {
3360
+ border-right: none;
3361
+ border-left: 1px solid rgba(255, 255, 255, 0.3);
3362
+ }
3363
+
3344
3364
  /* ============================================================
3345
3365
  Responsive: ensure container fills parent
3346
3366
  ============================================================ */
@@ -4,7 +4,7 @@
4
4
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.EBPlayer = {}));
5
5
  })(this, (function (exports) { 'use strict';
6
6
 
7
- var __EB_PLAYER_VERSION__ = "2.2.0";
7
+ var __EB_PLAYER_VERSION__ = "2.3.1";
8
8
 
9
9
  /**
10
10
  * Finite State Machine for player playback state transitions.
@@ -1525,7 +1525,7 @@
1525
1525
  return null;
1526
1526
  }
1527
1527
  // ---- EPG helpers ----
1528
- renderEpgSegments(programs) {
1528
+ renderEpgSegments(programs, isRtl) {
1529
1529
  if (programs.length === 0)
1530
1530
  return [];
1531
1531
  // Compute the time window from first program start to last program end
@@ -1536,13 +1536,18 @@
1536
1536
  return [];
1537
1537
  const now = Date.now();
1538
1538
  return programs.map((program) => {
1539
- const leftPercent = ((program.start - windowStart) / windowDuration) * 100;
1539
+ const startPercent = ((program.start - windowStart) / windowDuration) * 100;
1540
1540
  const widthPercent = ((program.end - program.start) / windowDuration) * 100;
1541
1541
  const isCurrent = now >= program.start && now < program.end;
1542
+ // RTL mirrors the timeline: anchor from the track's inline-start edge
1543
+ // (right in RTL) instead of always anchoring from the physical left.
1544
+ const positionStyle = isRtl
1545
+ ? `right: ${startPercent.toFixed(2)}%; width: ${widthPercent.toFixed(2)}%`
1546
+ : `left: ${startPercent.toFixed(2)}%; width: ${widthPercent.toFixed(2)}%`;
1542
1547
  return b `
1543
1548
  <div
1544
1549
  class="eb-epg-segment${isCurrent ? ' eb-epg-current' : ''}"
1545
- style="left: ${leftPercent.toFixed(2)}%; width: ${widthPercent.toFixed(2)}%"
1550
+ style="${positionStyle}"
1546
1551
  title="${program.title}"
1547
1552
  ></div>
1548
1553
  `;
@@ -1550,7 +1555,7 @@
1550
1555
  }
1551
1556
  // ---- Template ----
1552
1557
  template() {
1553
- const { currentTime, duration, bufferedEnd, adPlaying, introStreamPlaying, chapters, epgPrograms } = this.state;
1558
+ const { currentTime, duration, bufferedEnd, adPlaying, introStreamPlaying, chapters, epgPrograms, isRtl } = this.state;
1554
1559
  const isSeekbarHidden = !this.config.seekbar;
1555
1560
  const isDisabled = adPlaying || introStreamPlaying;
1556
1561
  // Progress percentage — use dragValue during drag to prevent live-update jitter
@@ -1563,12 +1568,17 @@
1563
1568
  const activeChapter = this.findActiveChapter(chapters);
1564
1569
  const skippableEntry = activeChapter !== null ? this.findSkippableChapter(activeChapter) : null;
1565
1570
  const chapterMarkers = chapters.map((chapter) => {
1566
- const leftPercent = duration > 0 ? (chapter.startTime / duration) * 100 : 0;
1571
+ const positionPercent = duration > 0 ? (chapter.startTime / duration) * 100 : 0;
1567
1572
  const isActive = chapter === activeChapter;
1573
+ // RTL mirrors the timeline: anchor from the track's inline-start edge
1574
+ // (right in RTL) instead of always anchoring from the physical left.
1575
+ const positionStyle = isRtl
1576
+ ? `right: ${positionPercent.toFixed(2)}%`
1577
+ : `left: ${positionPercent.toFixed(2)}%`;
1568
1578
  return b `
1569
1579
  <div
1570
1580
  class="eb-chapter-marker${isActive ? ' eb-chapter-active' : ''}"
1571
- style="left: ${leftPercent.toFixed(2)}%"
1581
+ style="${positionStyle}"
1572
1582
  title="${chapter.title}"
1573
1583
  ></div>
1574
1584
  `;
@@ -1583,7 +1593,7 @@
1583
1593
  `
1584
1594
  : b ``;
1585
1595
  // EPG segments
1586
- const epgSegments = this.renderEpgSegments(epgPrograms);
1596
+ const epgSegments = this.renderEpgSegments(epgPrograms, isRtl);
1587
1597
  // Tooltip — for live streams, show wall-clock time at hovered position
1588
1598
  const tooltipTimeText = this.state.isLive && Number.isFinite(duration)
1589
1599
  ? formatWallClock(Date.now() - (duration - this.tooltipTime) * 1000)
@@ -4388,6 +4398,170 @@
4388
4398
  };
4389
4399
  }
4390
4400
 
4401
+ var subtitlesParserVtt = {exports: {}};
4402
+
4403
+ var hasRequiredSubtitlesParserVtt;
4404
+
4405
+ function requireSubtitlesParserVtt () {
4406
+ if (hasRequiredSubtitlesParserVtt) return subtitlesParserVtt.exports;
4407
+ hasRequiredSubtitlesParserVtt = 1;
4408
+ (function (module, exports) {
4409
+ var parser = (function () {
4410
+ var pItems = {};
4411
+
4412
+ /**
4413
+ * Converts SubRip subtitles into array of objects
4414
+ * [{
4415
+ * id: `Number of subtitle`
4416
+ * startTime: `Start time of subtitle`
4417
+ * endTime: `End time of subtitle
4418
+ * text: `Text of subtitle`
4419
+ * }]
4420
+ *
4421
+ * @param {String} data SubRip suntitles string
4422
+ * @param {Boolean} ms Optional: use milliseconds for startTime and endTime
4423
+ * @return {Array}
4424
+ */
4425
+ pItems.fromVtt = function (data, timeFormat, isYoutubeAutoTranscript) {
4426
+ var useYoutubeAutoTranscript = isYoutubeAutoTranscript ? true : false;
4427
+ data = data.replace(/\r/g, "");
4428
+ var regex = /(\d+)?\n?(\d{2}:\d{2}:\d{2}[,.]\d{3}) --> (\d{2}:\d{2}:\d{2}[,.]\d{3}).*\n/g;
4429
+ data = data.split(regex);
4430
+ data.shift();
4431
+ var items = [];
4432
+ for (var i = 0; i < data.length; i += 4) {
4433
+ var text = data[i + 3];
4434
+ if (useYoutubeAutoTranscript) {
4435
+ text = text.split("\n");
4436
+ text.shift();
4437
+ text = text.join("\n");
4438
+ }
4439
+ if (text.trim().length === 0) continue;
4440
+ items.push({
4441
+ id: data[i] ? +data[i].trim() : items.length + 1,
4442
+ startTime: changeTimeFormat(data[i + 1].trim(), timeFormat),
4443
+ endTime: changeTimeFormat(data[i + 2].trim(), timeFormat),
4444
+ text: text.trim(),
4445
+ });
4446
+ }
4447
+ return items;
4448
+ };
4449
+
4450
+ /**
4451
+ * Converts Array of objects created by this module to SubRip subtitles
4452
+ * @param {Array} data
4453
+ * @return {String} SubRip subtitles string
4454
+ */
4455
+ pItems.toVtt = function (data) {
4456
+ if (!data instanceof Array) return "";
4457
+ var res = "";
4458
+
4459
+ for (var i = 0; i < data.length; i++) {
4460
+ var s = data[i];
4461
+
4462
+ if (!isNaN(s.startTime) && !isNaN(s.endTime)) {
4463
+ s.startTime = msTime(parseInt(s.startTime, 10));
4464
+ s.endTime = msTime(parseInt(s.endTime, 10));
4465
+ }
4466
+
4467
+ res += s.id + "\r\n";
4468
+ res += s.startTime + " --> " + s.endTime + "\r\n";
4469
+ res += s.text.replace("\n", "\r\n") + "\r\n\r\n";
4470
+ }
4471
+
4472
+ return res;
4473
+ };
4474
+
4475
+ var changeTimeFormat = function (time, format) {
4476
+ if (format === "ms") {
4477
+ return timeMs(time);
4478
+ } else if (format === "s") {
4479
+ return timeMs(time) / 1000;
4480
+ } else {
4481
+ return time;
4482
+ }
4483
+ };
4484
+
4485
+ var timeMs = function (val) {
4486
+ var regex = /(\d+):(\d{2}):(\d{2})[,.](\d{3})/;
4487
+ var parts = regex.exec(val);
4488
+
4489
+ if (parts === null) {
4490
+ return 0;
4491
+ }
4492
+
4493
+ for (var i = 1; i < 5; i++) {
4494
+ parts[i] = parseInt(parts[i], 10);
4495
+ if (isNaN(parts[i])) parts[i] = 0;
4496
+ }
4497
+
4498
+ // hours + minutes + seconds + ms
4499
+ return parts[1] * 3600000 + parts[2] * 60000 + parts[3] * 1000 + parts[4];
4500
+ };
4501
+
4502
+ var msTime = function (val) {
4503
+ var measures = [3600000, 60000, 1000];
4504
+ var time = [];
4505
+
4506
+ for (var i in measures) {
4507
+ var res = ((val / measures[i]) >> 0).toString();
4508
+
4509
+ if (res.length < 2) res = "0" + res;
4510
+ val %= measures[i];
4511
+ time.push(res);
4512
+ }
4513
+
4514
+ var ms = val.toString();
4515
+ if (ms.length < 3) {
4516
+ for (i = 0; i <= 3 - ms.length; i++) ms = "0" + ms;
4517
+ }
4518
+
4519
+ return time.join(":") + "," + ms;
4520
+ };
4521
+
4522
+ return pItems;
4523
+ })();
4524
+
4525
+ // ignore exports for browser
4526
+ {
4527
+ module.exports = parser;
4528
+ }
4529
+ } (subtitlesParserVtt));
4530
+ return subtitlesParserVtt.exports;
4531
+ }
4532
+
4533
+ var subtitlesParserVttExports = requireSubtitlesParserVtt();
4534
+
4535
+ /**
4536
+ * Parses a WebVTT string into an array of ChapterMark objects.
4537
+ * Returns an empty array if the content is empty or contains no parseable cues.
4538
+ */
4539
+ function parseChapters(vttContent) {
4540
+ if (!vttContent || !vttContent.trim()) {
4541
+ return [];
4542
+ }
4543
+ const cues = subtitlesParserVttExports.fromVtt(vttContent, 's');
4544
+ return cues.map((cue) => ({
4545
+ title: cue.text,
4546
+ startTime: cue.startTime,
4547
+ endTime: cue.endTime
4548
+ }));
4549
+ }
4550
+ /**
4551
+ * Fetches a WebVTT chapters file from a URL and parses it.
4552
+ * Returns an empty array if the fetch or parse fails.
4553
+ */
4554
+ async function fetchChapters(url) {
4555
+ try {
4556
+ const response = await fetch(url);
4557
+ const text = await response.text();
4558
+ return parseChapters(text);
4559
+ }
4560
+ catch {
4561
+ return [];
4562
+ }
4563
+ }
4564
+
4391
4565
  /**
4392
4566
  * PlayerController manages the complete lifecycle of a player instance.
4393
4567
  *
@@ -4588,6 +4762,25 @@
4588
4762
  // CRITICAL: AbortController is one-shot. Create a fresh one for re-mount.
4589
4763
  this.abortController = new AbortController();
4590
4764
  }
4765
+ /**
4766
+ * Populate state.chapters from config.chapters.
4767
+ *
4768
+ * The value is either an inline WebVTT string or an http(s) URL pointing at a
4769
+ * .vtt file. A remote fetch is fire-and-forget and non-critical: fetchChapters
4770
+ * resolves to an empty array on failure, and a result arriving after dispose()
4771
+ * is discarded rather than written into a torn-down state.
4772
+ */
4773
+ loadChapters(source, state, signal) {
4774
+ if (/^https?:\/\//i.test(source.trim())) {
4775
+ void fetchChapters(source).then((chapters) => {
4776
+ if (!signal.aborted) {
4777
+ state.chapters = chapters;
4778
+ }
4779
+ });
4780
+ return;
4781
+ }
4782
+ state.chapters = parseChapters(source);
4783
+ }
4591
4784
  /**
4592
4785
  * Initialize integration managers based on config flags.
4593
4786
  * All integrations are async and non-critical — errors are caught and logged.
@@ -4630,6 +4823,10 @@
4630
4823
  });
4631
4824
  }
4632
4825
  }
4826
+ // Chapters: skip when config.chapters is not set
4827
+ if (config.chapters) {
4828
+ this.loadChapters(config.chapters, state, signal);
4829
+ }
4633
4830
  // IntroStreamManager: skip when config.preroll is false (D-02)
4634
4831
  if (config.preroll && this.skinRoot !== null) {
4635
4832
  const introManager = new IntroStreamManager();