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();
+});