From 8f3f32cb2ba5a425499d4100e4d2b19d0661c69d Mon Sep 17 00:00:00 2001 From: Usman Omar Date: Mon, 6 Mar 2023 09:51:59 +0000 Subject: [PATCH] feat: add skip forward/backward buttons (#8147) * remove duplicate icons from icon example * create initial forward and back button classes * add logic for back/forward buttons on click * change icon used based on option passed into player * move logic from forward and back buttons into one component * add jsdoc comments for clarity * create initial test file * refactor button logic into separate files * update skip button example and add test files * test both the forward and backward buttons * test handleClick fns for both forward and backward btns * update skip buttons example * update jsdocs for skip backward and forward buttons * make control text accessible and use seekableEnd/Start when skipping forward/back * update font version to use updated icons * set control text only if config is valid * add link to sandbox page & use localization * update translations needed --- docs/translations-needed.md | 178 +++++++++++++----- index.html | 1 + lang/en.json | 4 +- package-lock.json | 121 ++++++++---- package.json | 2 +- sandbox/icons.html.example | 3 - sandbox/skip-buttons.html.example | 114 +++++++++++ src/css/components/_skip-buttons.scss | 40 ++++ src/css/video-js.scss | 1 + src/js/control-bar/control-bar.js | 4 + .../control-bar/skip-buttons/skip-backward.js | 69 +++++++ .../control-bar/skip-buttons/skip-forward.js | 66 +++++++ .../skip-buttons/skip-backward-button.test.js | 98 ++++++++++ .../skip-buttons/skip-forward-button.test.js | 117 ++++++++++++ 14 files changed, 725 insertions(+), 93 deletions(-) create mode 100644 sandbox/skip-buttons.html.example create mode 100644 src/css/components/_skip-buttons.scss create mode 100644 src/js/control-bar/skip-buttons/skip-backward.js create mode 100644 src/js/control-bar/skip-buttons/skip-forward.js create mode 100644 test/unit/control-bar/skip-buttons/skip-backward-button.test.js create mode 100644 test/unit/control-bar/skip-buttons/skip-forward-button.test.js diff --git a/docs/translations-needed.md b/docs/translations-needed.md index 5568a6497..75ef07d18 100644 --- a/docs/translations-needed.md +++ b/docs/translations-needed.md @@ -16,8 +16,9 @@ This default value is hardcoded as a default to the localize method in the SeekB | Language file | Missing translations | | ----------------------- | ----------------------------------------------------------------------------------- | -| ar.json (Complete) | | -| ba.json (missing 68) | Audio Player | +| ar.json (missing 2) | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| ba.json (missing 70) | Audio Player | | | Video Player | | | Replay | | | Seek to live, currently behind live | @@ -85,7 +86,9 @@ This default value is hardcoded as a default to the localize method in the SeekB | | Opacity | | | Text Background | | | Caption Area Background | -| bg.json (missing 68) | Audio Player | +| | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| bg.json (missing 70) | Audio Player | | | Video Player | | | Replay | | | Seek to live, currently behind live | @@ -153,12 +156,16 @@ This default value is hardcoded as a default to the localize method in the SeekB | | Opacity | | | Text Background | | | Caption Area Background | -| bn.json (missing 5) | Exit Fullscreen | +| | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| bn.json (missing 7) | Exit Fullscreen | | | Color | | | Opacity | | | Text Background | | | Caption Area Background | -| ca.json (missing 68) | Audio Player | +| | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| ca.json (missing 70) | Audio Player | | | Video Player | | | Replay | | | Seek to live, currently behind live | @@ -226,7 +233,9 @@ This default value is hardcoded as a default to the localize method in the SeekB | | Opacity | | | Text Background | | | Caption Area Background | -| cs.json (missing 9) | Seek to live, currently behind live | +| | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| cs.json (missing 11) | Seek to live, currently behind live | | | Seek to live, currently playing live | | | Exit Picture-in-Picture | | | Picture-in-Picture | @@ -235,7 +244,9 @@ This default value is hardcoded as a default to the localize method in the SeekB | | Opacity | | | Text Background | | | Caption Area Background | -| cy.json (missing 9) | Seek to live, currently behind live | +| | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| cy.json (missing 11) | Seek to live, currently behind live | | | Seek to live, currently playing live | | | Exit Picture-in-Picture | | | Picture-in-Picture | @@ -244,7 +255,9 @@ This default value is hardcoded as a default to the localize method in the SeekB | | Opacity | | | Text Background | | | Caption Area Background | -| da.json (missing 68) | Audio Player | +| | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| da.json (missing 70) | Audio Player | | | Video Player | | | Replay | | | Seek to live, currently behind live | @@ -312,8 +325,11 @@ This default value is hardcoded as a default to the localize method in the SeekB | | Opacity | | | Text Background | | | Caption Area Background | -| de.json (Complete) | | -| el.json (missing 54) | Audio Player | +| | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| de.json (missing 2) | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| el.json (missing 56) | Audio Player | | | Video Player | | | Replay | | | Seek to live, currently behind live | @@ -367,24 +383,33 @@ This default value is hardcoded as a default to the localize method in the SeekB | | Opacity | | | Text Background | | | Caption Area Background | -| en-GB.json (has 1) | Needs manual checking. Can safely use most default strings. -| es.json (Complete) | | -| et.json (missing 5) | No content | +| | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| en-GB.json (has 1) | Needs manual checking. Can safely use most default strings. | +| es.json (missing 2) | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| et.json (missing 7) | No content | | | Color | | | Opacity | | | Text Background | | | Caption Area Background | -| eu.json (missing 5) | No content | +| | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| eu.json (missing 7) | No content | | | Color | | | Opacity | | | Text Background | | | Caption Area Background | -| fa.json (missing 5) | No content | +| | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| fa.json (missing 7) | No content | | | Color | | | Opacity | | | Text Background | | | Caption Area Background | -| fi.json (missing 68) | Audio Player | +| | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| fi.json (missing 70) | Audio Player | | | Video Player | | | Replay | | | Seek to live, currently behind live | @@ -452,22 +477,29 @@ This default value is hardcoded as a default to the localize method in the SeekB | | Opacity | | | Text Background | | | Caption Area Background | -| fr.json (Complete) | | -| gd.json (missing 7) | Exit Picture-in-Picture | +| | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| fr.json (missing 2) | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| gd.json (missing 9) | Exit Picture-in-Picture | | | Picture-in-Picture | | | No content | | | Color | | | Opacity | | | Text Background | | | Caption Area Background | -| gl.json (missing 7) | Exit Picture-in-Picture | +| | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| gl.json (missing 9) | Exit Picture-in-Picture | | | Picture-in-Picture | | | No content | | | Color | | | Opacity | | | Text Background | | | Caption Area Background | -| he.json (missing 10) | Seek to live, currently behind live | +| | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| he.json (missing 12) | Seek to live, currently behind live | | | Seek to live, currently playing live | | | {1} is loading. | | | Exit Picture-in-Picture | @@ -477,12 +509,16 @@ This default value is hardcoded as a default to the localize method in the SeekB | | Opacity | | | Text Background | | | Caption Area Background | -| hi.json (missing 5) | No content | +| | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| hi.json (missing 7) | No content | | | Color | | | Opacity | | | Text Background | | | Caption Area Background | -| hr.json (missing 68) | Audio Player | +| | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| hr.json (missing 70) | Audio Player | | | Video Player | | | Replay | | | Seek to live, currently behind live | @@ -550,33 +586,45 @@ This default value is hardcoded as a default to the localize method in the SeekB | | Opacity | | | Text Background | | | Caption Area Background | -| hu.json (missing 5) | No content | +| | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| hu.json (missing 7) | No content | | | Color | | | Opacity | | | Text Background | | | Caption Area Background | -| it.json (missing 7) | Seek to live, currently behind live | +| | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| it.json (missing 9) | Seek to live, currently behind live | | | Seek to live, currently playing live | | | Raised | | | Depressed | | | Casual | | | Script | | | No content | -| ja.json (Complete) | | -| ko.json (Complete) | | -| lv.json (missing 5) | No content | +| | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| ja.json (missing 2) | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| ko.json (missing 2) | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| lv.json (missing 7) | No content | | | Color | | | Opacity | | | Text Background | | | Caption Area Background | -| nb.json (missing 7) | Exit Picture-in-Picture | +| | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| nb.json (missing 9) | Exit Picture-in-Picture | | | Picture-in-Picture | | | No content | | | Color | | | Opacity | | | Text Background | | | Caption Area Background | -| nl.json (missing 10) | Seek to live, currently behind live | +| | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| nl.json (missing 12) | Seek to live, currently behind live | | | Seek to live, currently playing live | | | {1} is loading. | | | Exit Picture-in-Picture | @@ -586,29 +634,39 @@ This default value is hardcoded as a default to the localize method in the SeekB | | Opacity | | | Text Background | | | Caption Area Background | -| nn.json (missing 7) | Exit Picture-in-Picture | +| | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| nn.json (missing 9) | Exit Picture-in-Picture | | | Picture-in-Picture | | | No content | | | Color | | | Opacity | | | Text Background | | | Caption Area Background | -| oc.json (missing 4) | Color | +| | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| oc.json (missing 6) | Color | | | Opacity | | | Text Background | | | Caption Area Background | -| pl.json (missing 4) | Color | +| | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| pl.json (missing 6) | Color | | | Opacity | | | Text Background | | | Caption Area Background | -| pt-BR.json (missing 7) | Seek to live, currently behind live | +| | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| pt-BR.json (missing 9) | Seek to live, currently behind live | | | Seek to live, currently playing live | | | No content | | | Color | | | Opacity | | | Text Background | | | Caption Area Background | -| pt-PT.json (missing 53) | Audio Player | +| | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| pt-PT.json (missing 55) | Audio Player | | | Video Player | | | Seek to live, currently behind live | | | Seek to live, currently playing live | @@ -661,17 +719,23 @@ This default value is hardcoded as a default to the localize method in the SeekB | | Opacity | | | Text Background | | | Caption Area Background | -| ro.json (missing 5) | No content | +| | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| ro.json (missing 7) | No content | | | Color | | | Opacity | | | Text Background | | | Caption Area Background | -| ru.json (missing 5) | No content | +| | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| ru.json (missing 7) | No content | | | Color | | | Opacity | | | Text Background | | | Caption Area Background | -| sk.json (missing 9) | Seek to live, currently behind live | +| | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| sk.json (missing 11) | Seek to live, currently behind live | | | Seek to live, currently playing live | | | Exit Picture-in-Picture | | | Picture-in-Picture | @@ -680,7 +744,9 @@ This default value is hardcoded as a default to the localize method in the SeekB | | Opacity | | | Text Background | | | Caption Area Background | -| sl.json (missing 11) | Proportional Sans-Serif | +| | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| sl.json (missing 13) | Proportional Sans-Serif | | | Monospace Sans-Serif | | | Proportional Serif | | | Monospace Serif | @@ -691,7 +757,9 @@ This default value is hardcoded as a default to the localize method in the SeekB | | Opacity | | | Text Background | | | Caption Area Background | -| sr.json (missing 68) | Audio Player | +| | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| sr.json (missing 70) | Audio Player | | | Video Player | | | Replay | | | Seek to live, currently behind live | @@ -759,28 +827,38 @@ This default value is hardcoded as a default to the localize method in the SeekB | | Opacity | | | Text Background | | | Caption Area Background | -| sv.json (missing 7) | Exit Picture-in-Picture | +| | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| sv.json (missing 9) | Exit Picture-in-Picture | | | Picture-in-Picture | | | No content | | | Color | | | Opacity | | | Text Background | | | Caption Area Background | -| te.json (missing 5) | No content | +| | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| te.json (missing 7) | No content | | | Color | | | Opacity | | | Text Background | | | Caption Area Background | -| th.json (missing 5) | No content | +| | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| th.json (missing 7) | No content | | | Color | | | Opacity | | | Text Background | | | Caption Area Background | -| tr.json (missing 4) | Color | +| | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| tr.json (missing 6) | Color | | | Opacity | | | Text Background | | | Caption Area Background | -| uk.json (missing 9) | Seek to live, currently behind live | +| | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| uk.json (missing 11) | Seek to live, currently behind live | | | Seek to live, currently playing live | | | Exit Picture-in-Picture | | | Picture-in-Picture | @@ -789,7 +867,9 @@ This default value is hardcoded as a default to the localize method in the SeekB | | Opacity | | | Text Background | | | Caption Area Background | -| vi.json (missing 10) | Seek to live, currently behind live | +| | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| vi.json (missing 12) | Seek to live, currently behind live | | | Seek to live, currently playing live | | | {1} is loading. | | | Exit Picture-in-Picture | @@ -799,7 +879,11 @@ This default value is hardcoded as a default to the localize method in the SeekB | | Opacity | | | Text Background | | | Caption Area Background | -| zh-CN.json (Complete) | | -| zh-TW.json (Complete) | | +| | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| zh-CN.json (missing 2) | Skip backward {1} seconds | +| | Skip forward {1} seconds | +| zh-TW.json (missing 2) | Skip backward {1} seconds | +| | Skip forward {1} seconds | diff --git a/index.html b/index.html index 9a5cbe77d..911151746 100644 --- a/index.html +++ b/index.html @@ -27,6 +27,7 @@
  • QualityLevels Demo
  • Autoplay Tests
  • noUITitleAttributes Demo
  • +
  • Skip Buttons demo
  • Videojs debug build test page
  • diff --git a/lang/en.json b/lang/en.json index 308a0bb1e..6d42e9bc8 100644 --- a/lang/en.json +++ b/lang/en.json @@ -90,5 +90,7 @@ "Color": "Color", "Opacity": "Opacity", "Text Background": "Text Background", - "Caption Area Background": "Caption Area Background" + "Caption Area Background": "Caption Area Background", + "Skip backward {1} seconds": "Skip backward {1} seconds", + "Skip forward {1} seconds": "Skip forward {1} seconds" } diff --git a/package-lock.json b/package-lock.json index 18b8c7bc6..ed56e5902 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "video.js", - "version": "8.1.1", + "version": "8.0.4", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1664,9 +1664,9 @@ } }, "@videojs/http-streaming": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.0.2.tgz", - "integrity": "sha512-iSZkwTLGg3Rx78ypCCq/GsMME89ElNvU02xj7reCE2PlITMQjyYsER1w5AsySvT1A694u5yuSzEzLLGF1cL4pg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.0.0.tgz", + "integrity": "sha512-AdKmY/W2dyeJP0uALgMRmhLa4pbHMvE4OMlg6yQvufnqsz6jDFo1DYnZRv2ENDYrmVdnPH58Ehgu59053+OIhQ==", "requires": { "@babel/runtime": "^7.12.5", "@videojs/vhs-utils": "4.0.0", @@ -1674,19 +1674,8 @@ "global": "^4.4.0", "m3u8-parser": "^6.0.0", "mpd-parser": "^1.0.1", - "mux.js": "6.3.0", + "mux.js": "6.2.0", "video.js": "^7 || ^8" - }, - "dependencies": { - "mux.js": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/mux.js/-/mux.js-6.3.0.tgz", - "integrity": "sha512-/QTkbSAP2+w1nxV+qTcumSDN5PA98P0tjrADijIzQHe85oBK3Akhy9AHlH0ne/GombLMz1rLyvVsmrgRxoPDrQ==", - "requires": { - "@babel/runtime": "^7.11.2", - "global": "^4.4.0" - } - } } }, "@videojs/vhs-utils": { @@ -15201,39 +15190,89 @@ "dev": true }, "video.js": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/video.js/-/video.js-8.0.4.tgz", - "integrity": "sha512-fvvWauPanrKDps1HQGGL+9CIAK8G0YVwlNme0hvY0k3moXQryaRcJSLHIlPKV2j9ZFTHl32VbN43jL3TaUllfg==", + "version": "7.21.1", + "resolved": "https://registry.npmjs.org/video.js/-/video.js-7.21.1.tgz", + "integrity": "sha512-AvHfr14ePDHCfW5Lx35BvXk7oIonxF6VGhSxocmTyqotkQpxwYdmt4tnQSV7MYzNrYHb0GI8tJMt20NDkCQrxg==", "requires": { "@babel/runtime": "^7.12.5", - "@videojs/http-streaming": "3.0.0", - "@videojs/vhs-utils": "^4.0.0", + "@videojs/http-streaming": "2.15.1", + "@videojs/vhs-utils": "^3.0.4", "@videojs/xhr": "2.6.0", - "aes-decrypter": "^4.0.1", - "global": "4.4.0", - "keycode": "2.2.0", - "m3u8-parser": "^6.0.0", - "mpd-parser": "^1.0.1", - "mux.js": "^6.2.0", + "aes-decrypter": "3.1.3", + "global": "^4.4.0", + "keycode": "^2.2.0", + "m3u8-parser": "4.8.0", + "mpd-parser": "0.22.1", + "mux.js": "6.0.1", "safe-json-parse": "4.0.0", - "videojs-contrib-quality-levels": "3.0.0", "videojs-font": "3.2.0", - "videojs-vtt.js": "0.15.4" + "videojs-vtt.js": "^0.15.4" }, "dependencies": { "@videojs/http-streaming": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.0.0.tgz", - "integrity": "sha512-AdKmY/W2dyeJP0uALgMRmhLa4pbHMvE4OMlg6yQvufnqsz6jDFo1DYnZRv2ENDYrmVdnPH58Ehgu59053+OIhQ==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-2.15.1.tgz", + "integrity": "sha512-/uuN3bVkEeJAdrhu5Hyb19JoUo3CMys7yf2C1vUjeL1wQaZ4Oe8JrZzRrnWZ0rjvPgKfNLPXQomsRtgrMoRMJQ==", "requires": { "@babel/runtime": "^7.12.5", - "@videojs/vhs-utils": "4.0.0", - "aes-decrypter": "4.0.1", + "@videojs/vhs-utils": "3.0.5", + "aes-decrypter": "3.1.3", "global": "^4.4.0", - "m3u8-parser": "^6.0.0", - "mpd-parser": "^1.0.1", - "mux.js": "6.2.0", - "video.js": "^7 || ^8" + "m3u8-parser": "4.8.0", + "mpd-parser": "^0.22.1", + "mux.js": "6.0.1", + "video.js": "^6 || ^7" + } + }, + "@videojs/vhs-utils": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.5.tgz", + "integrity": "sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==", + "requires": { + "@babel/runtime": "^7.12.5", + "global": "^4.4.0", + "url-toolkit": "^2.2.1" + } + }, + "aes-decrypter": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-3.1.3.tgz", + "integrity": "sha512-VkG9g4BbhMBy+N5/XodDeV6F02chEk9IpgRTq/0bS80y4dzy79VH2Gtms02VXomf3HmyRe3yyJYkJ990ns+d6A==", + "requires": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^3.0.5", + "global": "^4.4.0", + "pkcs7": "^1.0.4" + } + }, + "m3u8-parser": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-4.8.0.tgz", + "integrity": "sha512-UqA2a/Pw3liR6Df3gwxrqghCP17OpPlQj6RBPLYygf/ZSQ4MoSgvdvhvt35qV+3NaaA0FSZx93Ix+2brT1U7cA==", + "requires": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^3.0.5", + "global": "^4.4.0" + } + }, + "mpd-parser": { + "version": "0.22.1", + "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-0.22.1.tgz", + "integrity": "sha512-fwBebvpyPUU8bOzvhX0VQZgSohncbgYwUyJJoTSNpmy7ccD2ryiCvM7oRkn/xQH5cv73/xU7rJSNCLjdGFor0Q==", + "requires": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^3.0.5", + "@xmldom/xmldom": "^0.8.3", + "global": "^4.4.0" + } + }, + "mux.js": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/mux.js/-/mux.js-6.0.1.tgz", + "integrity": "sha512-22CHb59rH8pWGcPGW5Og7JngJ9s+z4XuSlYvnxhLuc58cA1WqGDQPzuG8I+sPm1/p0CdgpzVTaKW408k5DNn8w==", + "requires": { + "@babel/runtime": "^7.11.2", + "global": "^4.4.0" } }, "videojs-font": { @@ -15252,9 +15291,9 @@ } }, "videojs-font": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-4.0.0.tgz", - "integrity": "sha512-sRXrizXF0zBMatXjg2vGpn63G26uH3XqwyZ9PjU2H9xqGm7fRSVYuxOJCUME6us/1rFl9yxkRKk31WTQ7XZkww==" + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-4.1.0.tgz", + "integrity": "sha512-X1LuPfLZPisPLrANIAKCknZbZu5obVM/ylfd1CN+SsCmPZQ3UMDPcvLTpPBJxcBuTpHQq2MO1QCFt7p8spnZ/w==" }, "videojs-generate-karma-config": { "version": "8.0.1", diff --git a/package.json b/package.json index b912f1614..36967f6d7 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "mux.js": "^6.2.0", "safe-json-parse": "4.0.0", "videojs-contrib-quality-levels": "3.0.0", - "videojs-font": "4.0.0", + "videojs-font": "4.1.0", "videojs-vtt.js": "0.15.4" }, "devDependencies": { diff --git a/sandbox/icons.html.example b/sandbox/icons.html.example index 23ff5ca15..7453a6d5c 100644 --- a/sandbox/icons.html.example +++ b/sandbox/icons.html.example @@ -48,9 +48,6 @@
  • .vjs-icon-forward-5
  • .vjs-icon-forward-10
  • .vjs-icon-forward-30
  • -
  • .vjs-icon-forward-30
  • -
  • .vjs-icon-forward-30
  • -
  • .vjs-icon-forward-30
  • .vjs-icon-audio
  • .vjs-next-item
  • .vjs-icon-previous-item
  • diff --git a/sandbox/skip-buttons.html.example b/sandbox/skip-buttons.html.example new file mode 100644 index 000000000..b921058f7 --- /dev/null +++ b/sandbox/skip-buttons.html.example @@ -0,0 +1,114 @@ + + + + + Video.js Sandbox + + + + +
    +

    Forward: 5, Backward: 10

    + + + + + +

    + To view this video please enable JavaScript, and consider upgrading to + a web browser that + supports HTML5 video +

    +
    +
    + +
    +

    Forward: 10, Backward: 30

    + + + + + +

    + To view this video please enable JavaScript, and consider upgrading to + a web browser that + supports HTML5 video +

    +
    +
    + +
    +

    Forward: 10

    + + + + + +

    + To view this video please enable JavaScript, and consider upgrading to + a web browser that + supports HTML5 video +

    +
    +
    + + + + diff --git a/src/css/components/_skip-buttons.scss b/src/css/components/_skip-buttons.scss new file mode 100644 index 000000000..8c9bc52e7 --- /dev/null +++ b/src/css/components/_skip-buttons.scss @@ -0,0 +1,40 @@ +.video-js .vjs-skip-forward-5 { + cursor: pointer; + & .vjs-icon-placeholder { + @extend .vjs-icon-forward-5; + } +} + +.video-js .vjs-skip-forward-10 { + cursor: pointer; + & .vjs-icon-placeholder { + @extend .vjs-icon-forward-10; + } +} +.video-js .vjs-skip-forward-30 { + cursor: pointer; + & .vjs-icon-placeholder { + @extend .vjs-icon-forward-30; + } +} + +.video-js .vjs-skip-backward-5 { + cursor: pointer; + & .vjs-icon-placeholder { + @extend .vjs-icon-replay-5; + } +} + +.video-js .vjs-skip-backward-10 { + cursor: pointer; + & .vjs-icon-placeholder { + @extend .vjs-icon-replay-10; + } +} + +.video-js .vjs-skip-backward-30 { + cursor: pointer; + & .vjs-icon-placeholder { + @extend .vjs-icon-replay-30; + } +} \ No newline at end of file diff --git a/src/css/video-js.scss b/src/css/video-js.scss index fad65b513..e35b01830 100644 --- a/src/css/video-js.scss +++ b/src/css/video-js.scss @@ -42,6 +42,7 @@ @import "components/adaptive"; @import "components/captions-settings"; @import "components/title-bar"; +@import "components/skip-buttons"; @import "print"; diff --git a/src/js/control-bar/control-bar.js b/src/js/control-bar/control-bar.js index 84d3449f3..f66590d77 100644 --- a/src/js/control-bar/control-bar.js +++ b/src/js/control-bar/control-bar.js @@ -16,6 +16,8 @@ import './progress-control/progress-control.js'; import './picture-in-picture-toggle.js'; import './fullscreen-toggle.js'; import './volume-panel.js'; +import './skip-buttons/skip-forward.js'; +import './skip-buttons/skip-backward.js'; import './text-track-controls/chapters-button.js'; import './text-track-controls/descriptions-button.js'; import './text-track-controls/subtitles-button.js'; @@ -55,6 +57,8 @@ class ControlBar extends Component { ControlBar.prototype.options_ = { children: [ 'playToggle', + 'skipBackward', + 'skipForward', 'volumePanel', 'currentTimeDisplay', 'timeDivider', diff --git a/src/js/control-bar/skip-buttons/skip-backward.js b/src/js/control-bar/skip-buttons/skip-backward.js new file mode 100644 index 000000000..be9820763 --- /dev/null +++ b/src/js/control-bar/skip-buttons/skip-backward.js @@ -0,0 +1,69 @@ +import Button from '../../button'; +import Component from '../../component'; + +/** + * Button to skip backward a configurable amount of time + * through a video. Renders in the control bar. + * + * * e.g. options: {controlBar: {skipButtons: backward: 5}} + * + * @extends Button + */ +class SkipBackward extends Button { + constructor(player, options) { + super(player, options); + + this.validOptions = [5, 10, 30]; + this.skipTime = this.getSkipBackwardTime(); + + if (this.skipTime && this.validOptions.includes(this.skipTime)) { + this.controlText(this.localize('Skip backward {1} seconds', [this.skipTime])); + this.show(); + } else { + this.hide(); + } + } + + getSkipBackwardTime() { + const playerOptions = this.options_.playerOptions; + + return playerOptions.controlBar && playerOptions.controlBar.skipButtons && playerOptions.controlBar.skipButtons.backward; + } + + buildCSSClass() { + return `vjs-skip-backward-${this.getSkipBackwardTime()} ${super.buildCSSClass()}`; + } + + /** + * On click, skips backward in the video by a configurable amount of seconds. + * If the current time in the video is less than the configured 'skip backward' time, + * skips to beginning of video or seekable range. + * + * Handle a click on a `SkipBackward` button + * + * @param {EventTarget~Event} event + * The `click` event that caused this function + * to be called + */ + handleClick(event) { + const currentVideoTime = this.player_.currentTime(); + const liveTracker = this.player_.liveTracker; + const seekableStart = liveTracker && liveTracker.isLive() && liveTracker.seekableStart(); + let newTime; + + if (seekableStart && (currentVideoTime - this.skipTime <= seekableStart)) { + newTime = seekableStart; + } else if (currentVideoTime >= this.skipTime) { + newTime = currentVideoTime - this.skipTime; + } else { + newTime = 0; + } + this.player_.currentTime(newTime); + } +} + +SkipBackward.prototype.controlText_ = 'Skip Backward'; + +Component.registerComponent('SkipBackward', SkipBackward); + +export default SkipBackward; diff --git a/src/js/control-bar/skip-buttons/skip-forward.js b/src/js/control-bar/skip-buttons/skip-forward.js new file mode 100644 index 000000000..9bd9238e6 --- /dev/null +++ b/src/js/control-bar/skip-buttons/skip-forward.js @@ -0,0 +1,66 @@ +import Button from '../../button'; +import Component from '../../component'; + +/** + * Button to skip forward a configurable amount of time + * through a video. Renders in the control bar. + * + * e.g. options: {controlBar: {skipButtons: forward: 5}} + * + * @extends Button + */ +class SkipForward extends Button { + constructor(player, options) { + super(player, options); + + this.validOptions = [5, 10, 30]; + this.skipTime = this.getSkipForwardTime(); + + if (this.skipTime && this.validOptions.includes(this.skipTime)) { + this.controlText(this.localize('Skip forward {1} seconds', [this.skipTime])); + this.show(); + } else { + this.hide(); + } + } + + getSkipForwardTime() { + const playerOptions = this.options_.playerOptions; + + return playerOptions.controlBar && playerOptions.controlBar.skipButtons && playerOptions.controlBar.skipButtons.forward; + } + + buildCSSClass() { + return `vjs-skip-forward-${this.getSkipForwardTime()} ${super.buildCSSClass()}`; + } + + /** + * On click, skips forward in the duration/seekable range by a configurable amount of seconds. + * If the time left in the duration/seekable range is less than the configured 'skip forward' time, + * skips to end of duration/seekable range. + * + * Handle a click on a `SkipForward` button + * + * @param {EventTarget~Event} event + * The `click` event that caused this function + * to be called + */ + handleClick(event) { + const currentVideoTime = this.player_.currentTime(); + const liveTracker = this.player_.liveTracker; + const duration = (liveTracker && liveTracker.isLive()) ? liveTracker.seekableEnd() : this.player_.duration(); + let newTime; + + if (currentVideoTime + this.skipTime <= duration) { + newTime = currentVideoTime + this.skipTime; + } else { + newTime = duration; + } + + this.player_.currentTime(newTime); + } +} + +Component.registerComponent('SkipForward', SkipForward); + +export default SkipForward; diff --git a/test/unit/control-bar/skip-buttons/skip-backward-button.test.js b/test/unit/control-bar/skip-buttons/skip-backward-button.test.js new file mode 100644 index 000000000..732ece31b --- /dev/null +++ b/test/unit/control-bar/skip-buttons/skip-backward-button.test.js @@ -0,0 +1,98 @@ +/* eslint-env qunit */ +import TestHelpers from '../../test-helpers'; +import sinon from 'sinon'; +import { createTimeRange } from '../../../../src/js/utils/time'; + +QUnit.module('SkipBackwardButton'); + +QUnit.test('is not visible if option is not set', function(assert) { + const player = TestHelpers.makePlayer({}); + const button = player.controlBar.skipBackward; + + assert.expect(1); + assert.ok(button.hasClass('vjs-hidden'), 'has the vjs-hidden class'); + + player.dispose(); +}); + +QUnit.test('is not visible if option is set with an invalid config', function(assert) { + const player = TestHelpers.makePlayer({controlBar: {skipButtons: {backward: 4}}}); + const button = player.controlBar.skipBackward; + + assert.expect(1); + assert.ok(button.hasClass('vjs-hidden'), 'has the vjs-hidden class'); + + player.dispose(); +}); + +QUnit.test('is visible if option is set with a valid config', function(assert) { + const player = TestHelpers.makePlayer({controlBar: {skipButtons: {backward: 5}}}); + const button = player.controlBar.skipBackward; + + assert.expect(2); + assert.notOk(button.hasClass('vjs-hidden'), 'button is not hidden'); + assert.ok(button.hasClass('vjs-skip-backward-5'), 'button shows correct icon'); + + player.dispose(); +}); + +QUnit.test('control text should specify amount seconds that can be skipped backward', function(assert) { + const player = TestHelpers.makePlayer({controlBar: {skipButtons: {backward: 10}}}); + const button = player.controlBar.skipBackward; + + assert.expect(1); + assert.strictEqual(button.controlText_, 'Skip backward 10 seconds', 'control text specifies number of seconds backward'); + + player.dispose(); +}); + +QUnit.test('skip to beginning of seekable range in live video if current time - seekableStart is less than skip bacward time', function(assert) { + const player = TestHelpers.makePlayer({controlBar: {skipButtons: {backward: 30}}}); + const button = player.controlBar.skipBackward; + + player.options_.liveui = true; + player.seekable = () => createTimeRange(20, 40); + player.duration(Infinity); + player.currentTime(22); + + const curTimeSpy = sinon.spy(player, 'currentTime'); + + button.trigger('click'); + + assert.expect(1); + assert.equal(curTimeSpy.getCall(1).args[0], 20, 'player set to start of seekable range'); + + player.dispose(); +}); + +QUnit.test('skips to beginning of video if current time is less than configured skip backward time', function(assert) { + const player = TestHelpers.makePlayer({controlBar: {skipButtons: {backward: 30}}}); + const button = player.controlBar.skipBackward; + + player.currentTime(25); + + const curTimeSpy = sinon.spy(player, 'currentTime'); + + button.trigger('click'); + + assert.expect(1); + assert.equal(curTimeSpy.getCall(1).args[0], 0, 'player current time is set to start of video'); + + player.dispose(); +}); + +QUnit.test('skip backward in video by configured skip backward time amount', function(assert) { + const player = TestHelpers.makePlayer({controlBar: {skipButtons: {backward: 30}}}); + const button = player.controlBar.skipBackward; + + player.currentTime(31); + + const curTimeSpy = sinon.spy(player, 'currentTime'); + + button.trigger('click'); + + assert.expect(1); + assert.equal(curTimeSpy.getCall(1).args[0], 1, 'player current time set 30 seconds back on button click'); + + player.dispose(); +}); diff --git a/test/unit/control-bar/skip-buttons/skip-forward-button.test.js b/test/unit/control-bar/skip-buttons/skip-forward-button.test.js new file mode 100644 index 000000000..a654382fb --- /dev/null +++ b/test/unit/control-bar/skip-buttons/skip-forward-button.test.js @@ -0,0 +1,117 @@ +/* eslint-env qunit */ +import TestHelpers from '../../test-helpers'; +import sinon from 'sinon'; +import { createTimeRange } from '../../../../src/js/utils/time'; + +QUnit.module('SkipForwardButton'); + +QUnit.test('is not visible if option is not set', function(assert) { + const player = TestHelpers.makePlayer({}); + const button = player.controlBar.skipForward; + + assert.expect(1); + assert.ok(button.hasClass('vjs-hidden'), 'has the vjs-hidden class'); + + player.dispose(); +}); + +QUnit.test('is not visible if option is set with an invalid config', function(assert) { + const player = TestHelpers.makePlayer({controlBar: {skipButtons: {forward: 4}}}); + const button = player.controlBar.skipForward; + + assert.expect(1); + assert.ok(button.hasClass('vjs-hidden'), 'has the vjs-hidden class'); + + player.dispose(); +}); + +QUnit.test('is visible if option is set with a valid config', function(assert) { + const player = TestHelpers.makePlayer({controlBar: {skipButtons: {forward: 5}}}); + const button = player.controlBar.skipForward; + + assert.expect(2); + assert.notOk(button.hasClass('vjs-hidden'), 'button is not hidden'); + assert.ok(button.hasClass('vjs-skip-forward-5'), 'button shows correct icon'); + + player.dispose(); +}); + +QUnit.test('control text should specify how many seconds forward can be skipped', function(assert) { + const player = TestHelpers.makePlayer({controlBar: {skipButtons: {forward: 5}}}); + const button = player.controlBar.skipForward; + + assert.expect(1); + assert.strictEqual(button.controlText_, 'Skip forward 5 seconds', 'control text specifies seconds forward'); + + player.dispose(); +}); + +QUnit.test('skips to end of video if time remaining is less than configured skip forward time', function(assert) { + const player = TestHelpers.makePlayer({controlBar: {skipButtons: {forward: 30}}}); + const button = player.controlBar.skipForward; + + player.currentTime(25); + player.duration(30); + + const curTimeSpy = sinon.spy(player, 'currentTime'); + + button.trigger('click'); + + assert.expect(1); + assert.equal(curTimeSpy.getCall(1).args[0], 30, 'player current time is set to end of video'); + + player.dispose(); +}); + +QUnit.test('skips to end of seekable range in live video if time remaining is less than configured skip forward time', function(assert) { + const player = TestHelpers.makePlayer({controlBar: {skipButtons: {forward: 30}}}); + const button = player.controlBar.skipForward; + + player.options_.liveui = true; + player.seekable = () => createTimeRange(0, 20); + player.duration(Infinity); + + const curTimeSpy = sinon.spy(player, 'currentTime'); + + button.trigger('click'); + + assert.expect(1); + assert.equal(curTimeSpy.getCall(1).args[0], 20, 'player current time is set to end of seekable range in live video'); + + player.dispose(); +}); + +QUnit.test('skips forward in live video by configured skip forward time amount', function(assert) { + const player = TestHelpers.makePlayer({controlBar: {skipButtons: {forward: 10}}}); + const button = player.controlBar.skipForward; + + player.options_.liveui = true; + player.seekable = () => createTimeRange(0, 45); + player.duration(Infinity); + + const curTimeSpy = sinon.spy(player, 'currentTime'); + + button.trigger('click'); + + assert.expect(1); + assert.equal(curTimeSpy.getCall(1).args[0], 10, 'player current time is set 10 seconds forward in live video'); + + player.dispose(); +}); + +QUnit.test('skips forward in video by configured skip forward time amount', function(assert) { + const player = TestHelpers.makePlayer({controlBar: {skipButtons: {forward: 30}}}); + const button = player.controlBar.skipForward; + + player.currentTime(0); + player.duration(50); + + const curTimeSpy = sinon.spy(player, 'currentTime'); + + button.trigger('click'); + + assert.expect(1); + assert.equal(curTimeSpy.getCall(1).args[0], 30, 'player current time set 30 seconds forward after button click'); + + player.dispose(); +});