diff --git a/.github/workflows/javascript-tests.yml b/.github/workflows/javascript-tests.yml index 5f582966..c28c4804 100644 --- a/.github/workflows/javascript-tests.yml +++ b/.github/workflows/javascript-tests.yml @@ -36,9 +36,7 @@ jobs: sudo ln -sf /opt/firefox/firefox /usr/bin/firefox - name: Install Required System Packages - run: | - sudo apt-get update - sudo apt-get install -y libdbus-glib-1-2 libgtk-3-0 xvfb + run: sudo apt-get update && sudo apt-get install -y libxmlsec1-dev ubuntu-restricted-extras xvfb - name: Install npm dependencies run: npm ci diff --git a/docs/conf.py b/docs/conf.py index 17c0f81c..1e75f0ea 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -26,6 +26,10 @@ USE_TZ=True, LANGUAGE_CODE="en-us", LANGUAGES=[("en", "English")], + INSTALLED_APPS=[ + "edxval", + ], + TRANSCRIPT_LANG_CACHE_TIMEOUT=60 * 60 * 24, # 24 hours, required by edxval ) django.setup() diff --git a/package-lock.json b/package-lock.json index 6b8bd5eb..e6ca89fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -233,6 +233,13 @@ "node": ">=6.9.0" } }, + "node_modules/@bazel/runfiles": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@bazel/runfiles/-/runfiles-6.5.0.tgz", + "integrity": "sha512-RzahvqTkfpY2jsDxo8YItPX+/iZ6hbiikw1YhE0bA9EKBR5Og8Pa6FHn9PO9M0zaXRVsr0GFQLKbB/0rzy9SzA==", + "dev": true, + "peer": true + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -287,7 +294,6 @@ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -610,7 +616,6 @@ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -621,7 +626,6 @@ "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "license": "MIT", - "peer": true, "dependencies": { "@types/eslint": "*", "@types/estree": "*" @@ -631,8 +635,17 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "license": "MIT", - "peer": true + "license": "MIT" + }, + "node_modules/@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dev": true, + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } }, "node_modules/@types/json-schema": { "version": "7.0.15", @@ -640,12 +653,21 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "license": "MIT" }, + "node_modules/@types/minimatch": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-6.0.0.tgz", + "integrity": "sha512-zmPitbQ8+6zNutpwgcQuLcsEpn/Cj54Kbn7L5pX0Os5kdWplB7xPgEh/g+SWOB/qmows2gpuCaPyduq8ZZRnxA==", + "deprecated": "This is a stub types definition. minimatch provides its own type definitions, so you do not need this installed.", + "dev": true, + "dependencies": { + "minimatch": "*" + } + }, "node_modules/@types/node": { "version": "25.3.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.3.tgz", "integrity": "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.18.0" } @@ -655,7 +677,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" @@ -665,29 +686,25 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", @@ -698,15 +715,13 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -719,7 +734,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "license": "MIT", - "peer": true, "dependencies": { "@xtuc/ieee754": "^1.2.0" } @@ -729,7 +743,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@xtuc/long": "4.2.2" } @@ -738,15 +751,13 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -763,7 +774,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", @@ -777,7 +787,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -790,7 +799,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", @@ -805,7 +813,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" @@ -862,15 +869,13 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/accepts": { "version": "1.3.3", @@ -891,7 +896,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -904,7 +908,6 @@ "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=10.13.0" }, @@ -1078,6 +1081,27 @@ "node": ">=0.10.0" } }, + "node_modules/array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", + "dev": true, + "dependencies": { + "array-uniq": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/array-unique": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", @@ -1445,8 +1469,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/bytes": { "version": "3.1.2", @@ -1626,7 +1649,6 @@ "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=6.0" } @@ -1674,11 +1696,25 @@ "node": ">= 0.4" } }, + "node_modules/clean-webpack-plugin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/clean-webpack-plugin/-/clean-webpack-plugin-4.0.0.tgz", + "integrity": "sha512-WuWE1nyTNAyW5T7oNyys2EN0cfP2fdRxhxnIQWiAp0bMabPdHhoGxM8A6YL2GhqwgrPnnaemVE7nv5XJ2Fhh2w==", + "dev": true, + "dependencies": { + "del": "^4.1.1" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "webpack": ">=4.0.0 <6.0.0" + } + }, "node_modules/clone-deep": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, "license": "MIT", "dependencies": { "is-plain-object": "^2.0.4", @@ -1693,7 +1729,6 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -1761,8 +1796,7 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/common-path-prefix": { "version": "3.0.0", @@ -2001,6 +2035,24 @@ "node": ">=0.10.0" } }, + "node_modules/del": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", + "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==", + "dev": true, + "dependencies": { + "@types/glob": "^7.1.1", + "globby": "^6.1.0", + "is-path-cwd": "^2.0.0", + "is-path-in-cwd": "^2.0.0", + "p-map": "^2.0.0", + "pify": "^4.0.1", + "rimraf": "^2.6.3" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -2052,6 +2104,15 @@ "void-elements": "^2.0.0" } }, + "node_modules/draggabilly": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/draggabilly/-/draggabilly-3.0.0.tgz", + "integrity": "sha512-aEs+B6prbMZQMxc9lgTpCBfyCUhRur/VFucHhIOvlvvdARTj7TcDmX/cdOUtqbjJJUh7+agyJXR5Z6IFe1MxwQ==", + "dependencies": { + "get-size": "^3.0.0", + "unidragger": "^3.0.0" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2232,7 +2293,6 @@ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", "license": "MIT", - "peer": true, "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" @@ -2292,8 +2352,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/es-object-atoms": { "version": "1.1.1", @@ -2328,7 +2387,6 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "license": "BSD-2-Clause", - "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -2342,7 +2400,6 @@ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "license": "BSD-2-Clause", - "peer": true, "dependencies": { "estraverse": "^5.2.0" }, @@ -2355,7 +2412,6 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=4.0" } @@ -2365,11 +2421,15 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=4.0" } }, + "node_modules/ev-emitter": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ev-emitter/-/ev-emitter-2.1.2.tgz", + "integrity": "sha512-jQ5Ql18hdCQ4qS+RCrbLfz1n+Pags27q5TwMKvZyhp5hh2UULUYZUy1keqj6k6SYsdqIYjnmz7xyyEY0V67B8Q==" + }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -2382,7 +2442,6 @@ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.8.x" } @@ -2720,7 +2779,6 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true, "license": "BSD-3-Clause", "bin": { "flat": "cli.js" @@ -2898,6 +2956,11 @@ "node": ">= 0.4" } }, + "node_modules/get-size": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-size/-/get-size-3.0.0.tgz", + "integrity": "sha512-Y8aiXLq4leR7807UY0yuKEwif5s3kbVp1nTv+i4jBeoUzByTLKkLWu/HorS6/pB+7gsB0o7OTogC8AoOOeT0Hw==" + }, "node_modules/get-value": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", @@ -2994,8 +3057,32 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "license": "BSD-2-Clause", - "peer": true + "license": "BSD-2-Clause" + }, + "node_modules/globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw==", + "dev": true, + "dependencies": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/globby/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, "node_modules/gopd": { "version": "1.2.0", @@ -3177,6 +3264,11 @@ "node": ">= 0.4" } }, + "node_modules/hls.js": { + "version": "1.6.15", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.15.tgz", + "integrity": "sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==" + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -3243,6 +3335,13 @@ "node": ">=0.10.0" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true, + "peer": true + }, "node_modules/immutable": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", @@ -3608,6 +3707,39 @@ "node": ">=0.10.0" } }, + "node_modules/is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-path-in-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", + "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", + "dev": true, + "dependencies": { + "is-path-inside": "^2.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-path-inside": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", + "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", + "dev": true, + "dependencies": { + "path-is-inside": "^1.0.2" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -3832,7 +3964,6 @@ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -3847,7 +3978,6 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "license": "MIT", - "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -3865,6 +3995,15 @@ "deprecated": "This version is deprecated. Please upgrade to the latest version or find support at https://www.herodevs.com/support/jquery-nes.", "license": "MIT" }, + "node_modules/jquery-ui": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/jquery-ui/-/jquery-ui-1.14.2.tgz", + "integrity": "sha512-1gSl7PUjyipa2adSr780Ujk16faicrV7PjPPzPtvWk7tTqBnsqp67NNV9jZK2+BIxUPXWSnIUU/LBCgwgGZE+Q==", + "dev": true, + "dependencies": { + "jquery": ">=1.12.0 <5.0.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3887,8 +4026,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "1.0.0", @@ -3915,6 +4053,59 @@ "node": ">=6" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dev": true, + "peer": true, + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "peer": true + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "peer": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "peer": true + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "peer": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/karma": { "version": "0.13.22", "resolved": "https://registry.npmjs.org/karma/-/karma-0.13.22.tgz", @@ -3953,6 +4144,27 @@ "node": "0.10 || 0.12 || 4 || 5" } }, + "node_modules/karma-chrome-launcher": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz", + "integrity": "sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q==", + "dev": true, + "dependencies": { + "which": "^1.2.1" + } + }, + "node_modules/karma-chrome-launcher/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/karma-coverage": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.1.tgz", @@ -3992,6 +4204,47 @@ "jasmine-core": "*" } }, + "node_modules/karma-jasmine-html-reporter": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-0.2.2.tgz", + "integrity": "sha512-AHInTzedmNkyP8ue67p8lTy7DM6YUBfOX5VC3oexaUA0gY0L/2NErkl+aTd4QT9LYqg0VHTj6ie0LbMyulOwAw==", + "dev": true, + "dependencies": { + "karma-jasmine": "^1.0.2" + }, + "peerDependencies": { + "karma": ">=0.9" + } + }, + "node_modules/karma-jasmine-html-reporter/node_modules/karma-jasmine": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-1.1.2.tgz", + "integrity": "sha512-SENGE9DhlIIFTSZWiNq4eGeXL8G6z9cqHIOdkx9jh1qhhQqwEy3tAoLRyER0vOcHqdOlKmGpOuXk+HOipIy7sg==", + "dev": true, + "engines": { + "node": ">= 4" + }, + "peerDependencies": { + "jasmine-core": "*", + "karma": "*" + } + }, + "node_modules/karma-junit-reporter": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/karma-junit-reporter/-/karma-junit-reporter-2.0.1.tgz", + "integrity": "sha512-VtcGfE0JE4OE1wn0LK8xxDKaTP7slN8DO3I+4xg6gAi1IoAHAXOJ1V9G/y45Xg6sxdxPOR3THCFtDlAfBo9Afw==", + "dev": true, + "dependencies": { + "path-is-absolute": "^1.0.0", + "xmlbuilder": "12.0.0" + }, + "engines": { + "node": ">= 8" + }, + "peerDependencies": { + "karma": ">=0.9" + } + }, "node_modules/karma-requirejs": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/karma-requirejs/-/karma-requirejs-1.1.0.tgz", @@ -4003,6 +4256,18 @@ "requirejs": "^2.1.0" } }, + "node_modules/karma-selenium-webdriver-launcher": { + "version": "0.0.4-openedx.0", + "resolved": "git+ssh://git@github.com/openedx/karma-selenium-webdriver-launcher.git#79cfdc5037eb8585dd3e584875e4343febb6d61f", + "dev": true, + "dependencies": { + "q": "~0.9.6" + }, + "peerDependencies": { + "karma": ">=0.9", + "selenium-webdriver": ">=2.44.0" + } + }, "node_modules/karma-sourcemap-loader": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/karma-sourcemap-loader/-/karma-sourcemap-loader-0.4.0.tgz", @@ -4035,6 +4300,62 @@ "node": ">=0.1.90" } }, + "node_modules/karma-webpack": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/karma-webpack/-/karma-webpack-5.0.1.tgz", + "integrity": "sha512-oo38O+P3W2mSPCSUrQdySSPv1LvPpXP+f+bBimNomS5sW+1V4SuhCuW8TfJzV+rDv921w2fDSDw0xJbPe6U+kQ==", + "dev": true, + "dependencies": { + "glob": "^7.1.3", + "minimatch": "^9.0.3", + "webpack-merge": "^4.1.5" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/karma-webpack/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/karma-webpack/node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true + }, + "node_modules/karma-webpack/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/karma-webpack/node_modules/webpack-merge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.2.2.tgz", + "integrity": "sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g==", + "dev": true, + "dependencies": { + "lodash": "^4.17.15" + } + }, "node_modules/kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", @@ -4048,12 +4369,21 @@ "node": ">=0.10.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "peer": true, + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/loader-runner": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=6.11.5" }, @@ -4267,8 +4597,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/micromatch": { "version": "2.3.11", @@ -4562,8 +4891,7 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/node-addon-api": { "version": "7.1.1", @@ -4782,6 +5110,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -4792,6 +5129,13 @@ "node": ">=6" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "peer": true + }, "node_modules/parse-glob": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", @@ -4909,6 +5253,12 @@ "node": ">=0.10.0" } }, + "node_modules/path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", + "dev": true + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -4945,6 +5295,36 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "dev": true, + "dependencies": { + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/pkg-dir": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", @@ -5038,6 +5418,17 @@ "dev": true, "license": "MIT" }, + "node_modules/q": { + "version": "0.9.7", + "resolved": "https://registry.npmjs.org/q/-/q-0.9.7.tgz", + "integrity": "sha512-ijt0LhxWClXBtc1RCt8H0WhlZLAdVX26nWbpsJy+Hblmp81d2F/pFsvlrJhJDDruFHM+ECtxP0H0HzGSrARkwg==", + "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)", + "dev": true, + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, "node_modules/qs": { "version": "6.14.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", @@ -5763,6 +6154,64 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/selenium-webdriver": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.41.0.tgz", + "integrity": "sha512-1XxuKVhr9az24xwixPBEDGSZP+P0z3ZOnCmr9Oiep0MlJN2Mk+flIjD3iBS9BgyjS4g14dikMqnrYUPIjhQBhA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/SeleniumHQ" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/selenium" + } + ], + "peer": true, + "dependencies": { + "@bazel/runfiles": "^6.5.0", + "jszip": "^3.10.1", + "tmp": "^0.2.5", + "ws": "^8.19.0" + }, + "engines": { + "node": ">= 20.0.0" + } + }, + "node_modules/selenium-webdriver/node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "peer": true, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/selenium-webdriver/node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -5827,6 +6276,13 @@ "node": ">=0.10.0" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true, + "peer": true + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -5838,7 +6294,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, "license": "MIT", "dependencies": { "kind-of": "^6.0.2" @@ -5851,7 +6306,6 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6223,6 +6677,12 @@ "dev": true, "license": "MIT" }, + "node_modules/source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", + "dev": true + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -6261,7 +6721,6 @@ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "license": "MIT", - "peer": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -6272,7 +6731,6 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "license": "BSD-3-Clause", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -6415,7 +6873,6 @@ "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -6434,7 +6891,6 @@ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", @@ -6655,8 +7111,15 @@ "version": "7.18.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "license": "MIT", - "peer": true + "license": "MIT" + }, + "node_modules/unidragger": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/unidragger/-/unidragger-3.0.1.tgz", + "integrity": "sha512-RngbGSwBFmqGBWjkaH+yB677uzR95blSQyxq6hYbrQCejH3Mx1nm8DVOuh3M9k2fQyTstWUG5qlgCnNqV/9jVw==", + "dependencies": { + "ev-emitter": "^2.0.0" + } }, "node_modules/union-value": { "version": "1.0.1", @@ -6855,6 +7318,10 @@ "node": ">= 0.4.0" } }, + "node_modules/video-xblock": { + "resolved": "xblocks_contrib/video/assets", + "link": true + }, "node_modules/void-elements": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", @@ -6870,7 +7337,6 @@ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", "license": "MIT", - "peer": true, "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -6884,7 +7350,6 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.3.tgz", "integrity": "sha512-LLBBA4oLmT7sZdHiYE/PeVuifOxYyE2uL/V+9VQP7YSYdJU7bSf7H8bZRRxW8kEPMkmVjnrXmoR3oejIdX0xbg==", "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -6984,6 +7449,44 @@ "node": ">=14" } }, + "node_modules/webpack-manifest-plugin": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/webpack-manifest-plugin/-/webpack-manifest-plugin-5.0.1.tgz", + "integrity": "sha512-xTlX7dC3hrASixA2inuWFMz6qHsNi6MT3Uiqw621sJjRTShtpMjbDYhPPZBwWUKdIYKIjSq9em6+uzWayf38aQ==", + "dev": true, + "dependencies": { + "tapable": "^2.0.0", + "webpack-sources": "^2.2.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "webpack": "^5.75.0" + } + }, + "node_modules/webpack-manifest-plugin/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-manifest-plugin/node_modules/webpack-sources": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.1.tgz", + "integrity": "sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA==", + "dev": true, + "dependencies": { + "source-list-map": "^2.0.1", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/webpack-merge": { "version": "5.10.0", "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", @@ -7004,7 +7507,6 @@ "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=10.13.0" } @@ -7050,7 +7552,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", - "dev": true, "license": "MIT" }, "node_modules/wordwrap": { @@ -7088,6 +7589,15 @@ "dev": true, "license": "MIT" }, + "node_modules/xmlbuilder": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-12.0.0.tgz", + "integrity": "sha512-lMo8DJ8u6JRWp0/Y4XLa/atVDr75H9litKlb2E5j3V3MesoL50EBgZDWoLT3F/LztVnG67GjPXLZpqcky/UMnQ==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, "node_modules/xmlhttprequest-ssl": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.6.3.tgz", @@ -7147,6 +7657,52 @@ "string-replace-loader": "^3", "webpack-cli": "^5" } + }, + "xblocks_contrib/video/assets": { + "name": "video-xblock", + "version": "1.0.0", + "dependencies": { + "draggabilly": "^3", + "edx-ui-toolkit": "^1", + "hls.js": "^1", + "underscore": "^1", + "webpack-merge": "^6" + }, + "devDependencies": { + "clean-webpack-plugin": "^4", + "copy-webpack-plugin": "^13", + "jasmine-core": "2.6.4", + "jasmine-jquery": "git+https://git@github.com/velesin/jasmine-jquery.git#ebad463d592d3fea00c69f26ea18a930e09c7b58", + "jquery-ui": "^1.14.1", + "karma": "0.13", + "karma-chrome-launcher": "^3", + "karma-coverage": "^2", + "karma-firefox-launcher": "^2", + "karma-jasmine": "^0.3", + "karma-jasmine-html-reporter": "^0.2", + "karma-junit-reporter": "^2", + "karma-requirejs": "^1", + "karma-selenium-webdriver-launcher": "github:openedx/karma-selenium-webdriver-launcher#0.0.4-openedx.0", + "karma-sourcemap-loader": "^0.4", + "karma-spec-reporter": "^0.0", + "karma-webpack": "^5", + "webpack": "^5", + "webpack-cli": "^5", + "webpack-manifest-plugin": "^5" + } + }, + "xblocks_contrib/video/assets/node_modules/webpack-merge": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.1" + }, + "engines": { + "node": ">=18.0.0" + } } } } diff --git a/requirements/base.in b/requirements/base.in index 22557c64..2ea22114 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -12,6 +12,8 @@ edx-opaque-keys edx-submissions edx-toggles html5lib +edx-django-utils +edxval nh3 numpy oauthlib @@ -20,4 +22,5 @@ openedx-django-pyfs pillow random2 shapely +wrapt XBlock diff --git a/requirements/base.txt b/requirements/base.txt index 0cdc85c8..5c428a56 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -16,8 +16,18 @@ botocore==1.42.59 # via # boto3 # s3transfer +cachetools==7.0.2 + # via edxval +certifi==2026.2.25 + # via requests cffi==2.0.0 - # via pynacl + # via + # cryptography + # pynacl +chardet==6.0.0.post1 + # via pysrt +charset-normalizer==3.4.4 + # via requests chem==2.0.0 # via -r requirements/base.in click==8.3.1 @@ -25,8 +35,10 @@ click==8.3.1 # code-annotations # edx-django-utils # nltk -code-annotations==2.3.0 +code-annotations==2.3.2 # via edx-toggles +cryptography==46.0.5 + # via pyjwt ddt==1.7.2 # via -r requirements/base.in defusedxml==0.7.1 @@ -38,13 +50,17 @@ django==5.2.11 # django-crum # django-model-utils # django-statici18n + # django-storages # django-waffle # djangorestframework + # drf-jwt # edx-django-release-util # edx-django-utils + # edx-drf-extensions # edx-i18n-tools # edx-submissions # edx-toggles + # edxval # jsonfield # openedx-django-pyfs django-appconf==1.2.0 @@ -54,30 +70,53 @@ django-crum==0.7.9 # edx-django-utils # edx-toggles django-model-utils==5.0.0 - # via edx-submissions + # via + # edx-submissions + # edxval django-statici18n==2.6.0 # via -r requirements/base.in +django-storages==1.14.6 + # via edxval django-waffle==5.0.0 # via # edx-django-utils + # edx-drf-extensions # edx-toggles djangorestframework==3.16.1 - # via edx-submissions + # via + # drf-jwt + # edx-drf-extensions + # edx-submissions dnspython==2.8.0 # via pymongo +drf-jwt==1.19.2 + # via edx-drf-extensions edx-codejail==4.1.0 # via -r requirements/base.in edx-django-release-util==1.5.0 - # via edx-submissions + # via + # edx-submissions + # edxval edx-django-utils==8.0.1 - # via edx-toggles + # via + # -r requirements/base.in + # edx-drf-extensions + # edx-toggles +edx-drf-extensions==10.6.0 + # via edxval edx-i18n-tools==1.9.0 # via -r requirements/base.in edx-opaque-keys==3.1.0 - # via -r requirements/base.in + # via + # -r requirements/base.in + # edx-drf-extensions edx-submissions==3.12.2 # via -r requirements/base.in edx-toggles==5.4.1 + # via + # -r requirements/base.in + # edxval +edxval==3.2.0 # via -r requirements/base.in fs==2.4.16 # via @@ -88,6 +127,8 @@ fs-s3fs==1.1.1 # via openedx-django-pyfs html5lib==1.1 # via -r requirements/base.in +idna==3.11 + # via requests jinja2==3.1.6 # via code-annotations jmespath==1.1.0 @@ -101,6 +142,7 @@ jsonfield==3.2.0 lxml[html-clean]==6.0.2 # via # edx-i18n-tools + # edxval # lxml-html-clean # openedx-calc # xblock @@ -137,13 +179,19 @@ openedx-django-pyfs==3.8.0 path==16.16.0 # via edx-i18n-tools pillow==12.1.1 - # via -r requirements/base.in + # via + # -r requirements/base.in + # edxval polib==1.2.0 # via edx-i18n-tools psutil==7.2.2 # via edx-django-utils pycparser==3.0 # via cffi +pyjwt[crypto]==2.11.0 + # via + # drf-jwt + # edx-drf-extensions pymongo==4.16.0 # via edx-opaque-keys pynacl==1.6.2 @@ -152,13 +200,15 @@ pyparsing==3.3.2 # via # chem # openedx-calc +pysrt==1.1.2 + # via edxval python-dateutil==2.9.0.post0 # via # botocore # xblock python-slugify==8.0.4 # via code-annotations -pytz==2025.2 +pytz==2026.1.post1 # via # edx-submissions # xblock @@ -172,10 +222,14 @@ random2==1.0.2 # via -r requirements/base.in regex==2026.2.28 # via nltk +requests==2.32.5 + # via edx-drf-extensions s3transfer==0.16.0 # via boto3 scipy==1.17.1 # via chem +semantic-version==2.10.0 + # via edx-drf-extensions shapely==2.1.2 # via -r requirements/base.in simplejson==3.20.2 @@ -208,13 +262,17 @@ typing-extensions==4.15.0 # beautifulsoup4 # edx-opaque-keys urllib3==2.6.3 - # via botocore + # via + # botocore + # requests web-fragments==3.1.0 # via xblock webencodings==0.5.1 # via html5lib webob==1.8.9 # via xblock +wrapt==2.1.1 + # via -r requirements/base.in xblock==5.3.0 # via -r requirements/base.in diff --git a/requirements/dev.txt b/requirements/dev.txt index c211a60c..ce9d4c03 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -48,10 +48,11 @@ build==1.4.0 # via # -r requirements/pip-tools.txt # pip-tools -cachetools==7.0.1 +cachetools==7.0.2 # via # -r requirements/quality.txt # -r requirements/test.txt + # edxval # tox certifi==2026.2.25 # via @@ -62,6 +63,7 @@ cffi==2.0.0 # via # -r requirements/quality.txt # -r requirements/test.txt + # cryptography # pynacl chardet==6.0.0.post1 # via @@ -69,6 +71,7 @@ chardet==6.0.0.post1 # -r requirements/test.txt # binaryornot # diff-cover + # pysrt charset-normalizer==3.4.4 # via # -r requirements/quality.txt @@ -94,7 +97,7 @@ click-log==0.4.0 # via # -r requirements/quality.txt # edx-lint -code-annotations==2.3.0 +code-annotations==2.3.2 # via # -r requirements/quality.txt # -r requirements/test.txt @@ -105,7 +108,7 @@ colorama==0.4.6 # -r requirements/quality.txt # -r requirements/test.txt # tox -cookiecutter==2.6.0 +cookiecutter==2.7.0 # via # -r requirements/quality.txt # -r requirements/test.txt @@ -115,6 +118,11 @@ coverage[toml]==7.13.4 # -r requirements/quality.txt # -r requirements/test.txt # pytest-cov +cryptography==46.0.5 + # via + # -r requirements/quality.txt + # -r requirements/test.txt + # pyjwt ddt==1.7.2 # via # -r requirements/quality.txt @@ -143,13 +151,17 @@ django==5.2.11 # django-crum # django-model-utils # django-statici18n + # django-storages # django-waffle # djangorestframework + # drf-jwt # edx-django-release-util # edx-django-utils + # edx-drf-extensions # edx-i18n-tools # edx-submissions # edx-toggles + # edxval # jsonfield # openedx-django-pyfs # xblock-sdk @@ -169,26 +181,40 @@ django-model-utils==5.0.0 # -r requirements/quality.txt # -r requirements/test.txt # edx-submissions + # edxval django-statici18n==2.6.0 # via # -r requirements/quality.txt # -r requirements/test.txt +django-storages==1.14.6 + # via + # -r requirements/quality.txt + # -r requirements/test.txt + # edxval django-waffle==5.0.0 # via # -r requirements/quality.txt # -r requirements/test.txt # edx-django-utils + # edx-drf-extensions # edx-toggles djangorestframework==3.16.1 # via # -r requirements/quality.txt # -r requirements/test.txt + # drf-jwt + # edx-drf-extensions # edx-submissions dnspython==2.8.0 # via # -r requirements/quality.txt # -r requirements/test.txt # pymongo +drf-jwt==1.19.2 + # via + # -r requirements/quality.txt + # -r requirements/test.txt + # edx-drf-extensions edx-codejail==4.1.0 # via # -r requirements/quality.txt @@ -198,11 +224,18 @@ edx-django-release-util==1.5.0 # -r requirements/quality.txt # -r requirements/test.txt # edx-submissions + # edxval edx-django-utils==8.0.1 # via # -r requirements/quality.txt # -r requirements/test.txt + # edx-drf-extensions # edx-toggles +edx-drf-extensions==10.6.0 + # via + # -r requirements/quality.txt + # -r requirements/test.txt + # edxval edx-i18n-tools==1.9.0 # via # -r requirements/dev.in @@ -214,6 +247,7 @@ edx-opaque-keys==3.1.0 # via # -r requirements/quality.txt # -r requirements/test.txt + # edx-drf-extensions edx-submissions==3.12.2 # via # -r requirements/quality.txt @@ -222,6 +256,11 @@ edx-toggles==5.4.1 # via # -r requirements/quality.txt # -r requirements/test.txt + # edxval +edxval==3.2.0 + # via + # -r requirements/quality.txt + # -r requirements/test.txt filelock==3.25.0 # via # -r requirements/quality.txt @@ -288,6 +327,7 @@ lxml[html-clean]==6.0.2 # -r requirements/quality.txt # -r requirements/test.txt # edx-i18n-tools + # edxval # lxml-html-clean # openedx-calc # xblock @@ -378,6 +418,7 @@ pillow==12.1.1 # via # -r requirements/quality.txt # -r requirements/test.txt + # edxval pip-tools==7.5.3 # via -r requirements/pip-tools.txt platformdirs==4.9.2 @@ -422,6 +463,12 @@ pygments==2.19.2 # diff-cover # pytest # rich +pyjwt[crypto]==2.11.0 + # via + # -r requirements/quality.txt + # -r requirements/test.txt + # drf-jwt + # edx-drf-extensions pylint==4.0.5 # via # -r requirements/quality.txt @@ -473,6 +520,11 @@ pyproject-hooks==1.2.0 # -r requirements/pip-tools.txt # build # pip-tools +pysrt==1.1.2 + # via + # -r requirements/quality.txt + # -r requirements/test.txt + # edxval pytest==9.0.2 # via # -r requirements/quality.txt @@ -505,7 +557,7 @@ python-slugify==8.0.4 # -r requirements/test.txt # code-annotations # cookiecutter -pytz==2025.2 +pytz==2026.1.post1 # via # -r requirements/quality.txt # -r requirements/test.txt @@ -534,6 +586,7 @@ requests==2.32.5 # -r requirements/quality.txt # -r requirements/test.txt # cookiecutter + # edx-drf-extensions # xblock-sdk rich==14.3.3 # via @@ -550,6 +603,11 @@ scipy==1.17.1 # -r requirements/quality.txt # -r requirements/test.txt # chem +semantic-version==2.10.0 + # via + # -r requirements/quality.txt + # -r requirements/test.txt + # edx-drf-extensions shapely==2.1.2 # via # -r requirements/quality.txt @@ -658,6 +716,10 @@ wheel==0.46.3 # via # -r requirements/pip-tools.txt # pip-tools +wrapt==2.1.1 + # via + # -r requirements/quality.txt + # -r requirements/test.txt xblock==5.3.0 # via # -r requirements/quality.txt diff --git a/requirements/doc.txt b/requirements/doc.txt index 131e0914..7946b92a 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -45,9 +45,10 @@ botocore==1.42.59 # s3transfer build==1.4.0 # via -r requirements/doc.in -cachetools==7.0.1 +cachetools==7.0.2 # via # -r requirements/test.txt + # edxval # tox certifi==2026.2.25 # via @@ -56,11 +57,13 @@ certifi==2026.2.25 cffi==2.0.0 # via # -r requirements/test.txt + # cryptography # pynacl chardet==6.0.0.post1 # via # -r requirements/test.txt # binaryornot + # pysrt charset-normalizer==3.4.4 # via # -r requirements/test.txt @@ -74,7 +77,7 @@ click==8.3.1 # cookiecutter # edx-django-utils # nltk -code-annotations==2.3.0 +code-annotations==2.3.2 # via # -r requirements/test.txt # edx-toggles @@ -82,7 +85,7 @@ colorama==0.4.6 # via # -r requirements/test.txt # tox -cookiecutter==2.6.0 +cookiecutter==2.7.0 # via # -r requirements/test.txt # xblock-sdk @@ -90,6 +93,10 @@ coverage[toml]==7.13.4 # via # -r requirements/test.txt # pytest-cov +cryptography==46.0.5 + # via + # -r requirements/test.txt + # pyjwt ddt==1.7.2 # via -r requirements/test.txt defusedxml==0.7.1 @@ -106,13 +113,17 @@ django==5.2.11 # django-crum # django-model-utils # django-statici18n + # django-storages # django-waffle # djangorestframework + # drf-jwt # edx-django-release-util # edx-django-utils + # edx-drf-extensions # edx-i18n-tools # edx-submissions # edx-toggles + # edxval # jsonfield # openedx-django-pyfs # xblock-sdk @@ -129,16 +140,24 @@ django-model-utils==5.0.0 # via # -r requirements/test.txt # edx-submissions + # edxval django-statici18n==2.6.0 # via -r requirements/test.txt +django-storages==1.14.6 + # via + # -r requirements/test.txt + # edxval django-waffle==5.0.0 # via # -r requirements/test.txt # edx-django-utils + # edx-drf-extensions # edx-toggles djangorestframework==3.16.1 # via # -r requirements/test.txt + # drf-jwt + # edx-drf-extensions # edx-submissions dnspython==2.8.0 # via @@ -153,23 +172,39 @@ docutils==0.21.2 # readme-renderer # restructuredtext-lint # sphinx +drf-jwt==1.19.2 + # via + # -r requirements/test.txt + # edx-drf-extensions edx-codejail==4.1.0 # via -r requirements/test.txt edx-django-release-util==1.5.0 # via # -r requirements/test.txt # edx-submissions + # edxval edx-django-utils==8.0.1 # via # -r requirements/test.txt + # edx-drf-extensions # edx-toggles +edx-drf-extensions==10.6.0 + # via + # -r requirements/test.txt + # edxval edx-i18n-tools==1.9.0 # via -r requirements/test.txt edx-opaque-keys==3.1.0 - # via -r requirements/test.txt + # via + # -r requirements/test.txt + # edx-drf-extensions edx-submissions==3.12.2 # via -r requirements/test.txt edx-toggles==5.4.1 + # via + # -r requirements/test.txt + # edxval +edxval==3.2.0 # via -r requirements/test.txt filelock==3.25.0 # via @@ -196,7 +231,7 @@ idna==3.11 # via # -r requirements/test.txt # requests -imagesize==1.4.1 +imagesize==1.5.0 # via sphinx importlib-metadata==8.7.1 # via keyring @@ -235,6 +270,7 @@ lxml[html-clean]==6.0.2 # via # -r requirements/test.txt # edx-i18n-tools + # edxval # lxml-html-clean # openedx-calc # xblock @@ -307,7 +343,9 @@ path==16.16.0 # -r requirements/test.txt # edx-i18n-tools pillow==12.1.1 - # via -r requirements/test.txt + # via + # -r requirements/test.txt + # edxval platformdirs==4.9.2 # via # -r requirements/test.txt @@ -344,6 +382,11 @@ pygments==2.19.2 # readme-renderer # rich # sphinx +pyjwt[crypto]==2.11.0 + # via + # -r requirements/test.txt + # drf-jwt + # edx-drf-extensions pymongo==4.16.0 # via # -r requirements/test.txt @@ -367,6 +410,10 @@ pyproject-api==1.10.0 # tox pyproject-hooks==1.2.0 # via build +pysrt==1.1.2 + # via + # -r requirements/test.txt + # edxval pytest==9.0.2 # via # -r requirements/test.txt @@ -391,7 +438,7 @@ python-slugify==8.0.4 # -r requirements/test.txt # code-annotations # cookiecutter -pytz==2025.2 +pytz==2026.1.post1 # via # -r requirements/test.txt # edx-submissions @@ -416,6 +463,7 @@ requests==2.32.5 # via # -r requirements/test.txt # cookiecutter + # edx-drf-extensions # requests-toolbelt # sphinx # twine @@ -441,6 +489,10 @@ scipy==1.17.1 # via # -r requirements/test.txt # chem +semantic-version==2.10.0 + # via + # -r requirements/test.txt + # edx-drf-extensions shapely==2.1.2 # via -r requirements/test.txt simplejson==3.20.2 @@ -509,6 +561,10 @@ tqdm==4.67.3 # nltk twine==6.2.0 # via -r requirements/doc.in +types-python-dateutil==2.9.0.20251008 + # via + # -r requirements/test.txt + # arrow typing-extensions==4.15.0 # via # -r requirements/test.txt @@ -544,6 +600,8 @@ webob==1.8.9 # -r requirements/test.txt # xblock # xblock-sdk +wrapt==2.1.1 + # via -r requirements/test.txt xblock==5.3.0 # via # -r requirements/test.txt diff --git a/requirements/quality.txt b/requirements/quality.txt index 13567ccc..61f87c03 100644 --- a/requirements/quality.txt +++ b/requirements/quality.txt @@ -35,9 +35,10 @@ botocore==1.42.59 # -r requirements/test.txt # boto3 # s3transfer -cachetools==7.0.1 +cachetools==7.0.2 # via # -r requirements/test.txt + # edxval # tox certifi==2026.2.25 # via @@ -46,11 +47,13 @@ certifi==2026.2.25 cffi==2.0.0 # via # -r requirements/test.txt + # cryptography # pynacl chardet==6.0.0.post1 # via # -r requirements/test.txt # binaryornot + # pysrt charset-normalizer==3.4.4 # via # -r requirements/test.txt @@ -68,7 +71,7 @@ click==8.3.1 # nltk click-log==0.4.0 # via edx-lint -code-annotations==2.3.0 +code-annotations==2.3.2 # via # -r requirements/test.txt # edx-lint @@ -77,7 +80,7 @@ colorama==0.4.6 # via # -r requirements/test.txt # tox -cookiecutter==2.6.0 +cookiecutter==2.7.0 # via # -r requirements/test.txt # xblock-sdk @@ -85,6 +88,10 @@ coverage[toml]==7.13.4 # via # -r requirements/test.txt # pytest-cov +cryptography==46.0.5 + # via + # -r requirements/test.txt + # pyjwt ddt==1.7.2 # via -r requirements/test.txt defusedxml==0.7.1 @@ -103,13 +110,17 @@ django==5.2.11 # django-crum # django-model-utils # django-statici18n + # django-storages # django-waffle # djangorestframework + # drf-jwt # edx-django-release-util # edx-django-utils + # edx-drf-extensions # edx-i18n-tools # edx-submissions # edx-toggles + # edxval # jsonfield # openedx-django-pyfs # xblock-sdk @@ -126,40 +137,64 @@ django-model-utils==5.0.0 # via # -r requirements/test.txt # edx-submissions + # edxval django-statici18n==2.6.0 # via -r requirements/test.txt +django-storages==1.14.6 + # via + # -r requirements/test.txt + # edxval django-waffle==5.0.0 # via # -r requirements/test.txt # edx-django-utils + # edx-drf-extensions # edx-toggles djangorestframework==3.16.1 # via # -r requirements/test.txt + # drf-jwt + # edx-drf-extensions # edx-submissions dnspython==2.8.0 # via # -r requirements/test.txt # pymongo +drf-jwt==1.19.2 + # via + # -r requirements/test.txt + # edx-drf-extensions edx-codejail==4.1.0 # via -r requirements/test.txt edx-django-release-util==1.5.0 # via # -r requirements/test.txt # edx-submissions + # edxval edx-django-utils==8.0.1 # via # -r requirements/test.txt + # edx-drf-extensions # edx-toggles +edx-drf-extensions==10.6.0 + # via + # -r requirements/test.txt + # edxval edx-i18n-tools==1.9.0 # via -r requirements/test.txt edx-lint==5.6.0 # via -r requirements/quality.in edx-opaque-keys==3.1.0 - # via -r requirements/test.txt + # via + # -r requirements/test.txt + # edx-drf-extensions edx-submissions==3.12.2 # via -r requirements/test.txt edx-toggles==5.4.1 + # via + # -r requirements/test.txt + # edxval +edxval==3.2.0 # via -r requirements/test.txt filelock==3.25.0 # via @@ -214,6 +249,7 @@ lxml[html-clean]==6.0.2 # via # -r requirements/test.txt # edx-i18n-tools + # edxval # lxml-html-clean # openedx-calc # xblock @@ -278,7 +314,9 @@ path==16.16.0 # -r requirements/test.txt # edx-i18n-tools pillow==12.1.1 - # via -r requirements/test.txt + # via + # -r requirements/test.txt + # edxval platformdirs==4.9.2 # via # -r requirements/test.txt @@ -313,6 +351,11 @@ pygments==2.19.2 # -r requirements/test.txt # pytest # rich +pyjwt[crypto]==2.11.0 + # via + # -r requirements/test.txt + # drf-jwt + # edx-drf-extensions pylint==4.0.5 # via # edx-lint @@ -348,6 +391,10 @@ pyproject-api==1.10.0 # via # -r requirements/test.txt # tox +pysrt==1.1.2 + # via + # -r requirements/test.txt + # edxval pytest==9.0.2 # via # -r requirements/test.txt @@ -372,7 +419,7 @@ python-slugify==8.0.4 # -r requirements/test.txt # code-annotations # cookiecutter -pytz==2025.2 +pytz==2026.1.post1 # via # -r requirements/test.txt # edx-submissions @@ -395,6 +442,7 @@ requests==2.32.5 # via # -r requirements/test.txt # cookiecutter + # edx-drf-extensions # xblock-sdk rich==14.3.3 # via @@ -408,6 +456,10 @@ scipy==1.17.1 # via # -r requirements/test.txt # chem +semantic-version==2.10.0 + # via + # -r requirements/test.txt + # edx-drf-extensions shapely==2.1.2 # via -r requirements/test.txt simplejson==3.20.2 @@ -489,6 +541,8 @@ webob==1.8.9 # -r requirements/test.txt # xblock # xblock-sdk +wrapt==2.1.1 + # via -r requirements/test.txt xblock==5.3.0 # via # -r requirements/test.txt diff --git a/requirements/test.txt b/requirements/test.txt index a0820f64..044e8d3a 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -27,18 +27,29 @@ botocore==1.42.59 # -r requirements/base.txt # boto3 # s3transfer -cachetools==7.0.1 - # via tox +cachetools==7.0.2 + # via + # -r requirements/base.txt + # edxval + # tox certifi==2026.2.25 - # via requests + # via + # -r requirements/base.txt + # requests cffi==2.0.0 # via # -r requirements/base.txt + # cryptography # pynacl chardet==6.0.0.post1 - # via binaryornot + # via + # -r requirements/base.txt + # binaryornot + # pysrt charset-normalizer==3.4.4 - # via requests + # via + # -r requirements/base.txt + # requests chem==2.0.0 # via -r requirements/base.txt click==8.3.1 @@ -48,17 +59,21 @@ click==8.3.1 # cookiecutter # edx-django-utils # nltk -code-annotations==2.3.0 +code-annotations==2.3.2 # via # -r requirements/base.txt # -r requirements/test.in # edx-toggles colorama==0.4.6 # via tox -cookiecutter==2.6.0 +cookiecutter==2.7.0 # via xblock-sdk coverage[toml]==7.13.4 # via pytest-cov +cryptography==46.0.5 + # via + # -r requirements/base.txt + # pyjwt ddt==1.7.2 # via # -r requirements/base.txt @@ -74,13 +89,17 @@ distlib==0.4.0 # django-crum # django-model-utils # django-statici18n + # django-storages # django-waffle # djangorestframework + # drf-jwt # edx-django-release-util # edx-django-utils + # edx-drf-extensions # edx-i18n-tools # edx-submissions # edx-toggles + # edxval # jsonfield # openedx-django-pyfs # xblock-sdk @@ -97,40 +116,63 @@ django-model-utils==5.0.0 # via # -r requirements/base.txt # edx-submissions + # edxval django-statici18n==2.6.0 # via -r requirements/base.txt +django-storages==1.14.6 + # via + # -r requirements/base.txt + # edxval django-waffle==5.0.0 # via # -r requirements/base.txt # edx-django-utils + # edx-drf-extensions # edx-toggles djangorestframework==3.16.1 # via # -r requirements/base.txt + # drf-jwt + # edx-drf-extensions # edx-submissions dnspython==2.8.0 # via # -r requirements/base.txt # pymongo +drf-jwt==1.19.2 + # via + # -r requirements/base.txt + # edx-drf-extensions edx-codejail==4.1.0 # via -r requirements/base.txt edx-django-release-util==1.5.0 # via # -r requirements/base.txt # edx-submissions + # edxval edx-django-utils==8.0.1 # via # -r requirements/base.txt + # edx-drf-extensions # edx-toggles +edx-drf-extensions==10.6.0 + # via + # -r requirements/base.txt + # edxval edx-i18n-tools==1.9.0 # via -r requirements/base.txt edx-opaque-keys==3.1.0 # via # -r requirements/base.txt # -r requirements/test.in + # edx-drf-extensions edx-submissions==3.12.2 # via -r requirements/base.txt edx-toggles==5.4.1 + # via + # -r requirements/base.txt + # edxval +edxval==3.2.0 # via -r requirements/base.txt filelock==3.25.0 # via @@ -151,7 +193,9 @@ fs-s3fs==1.1.1 html5lib==1.1 # via -r requirements/base.txt idna==3.11 - # via requests + # via + # -r requirements/base.txt + # requests iniconfig==2.3.0 # via pytest jinja2==3.1.6 @@ -176,6 +220,7 @@ lxml[html-clean]==6.0.2 # via # -r requirements/base.txt # edx-i18n-tools + # edxval # lxml-html-clean # openedx-calc # xblock @@ -233,7 +278,9 @@ path==16.16.0 # -r requirements/base.txt # edx-i18n-tools pillow==12.1.1 - # via -r requirements/base.txt + # via + # -r requirements/base.txt + # edxval platformdirs==4.9.2 # via # python-discovery @@ -260,6 +307,11 @@ pygments==2.19.2 # via # pytest # rich +pyjwt[crypto]==2.11.0 + # via + # -r requirements/base.txt + # drf-jwt + # edx-drf-extensions pymongo==4.16.0 # via # -r requirements/base.txt @@ -277,6 +329,10 @@ pypng==0.20220715.0 # via xblock-sdk pyproject-api==1.10.0 # via tox +pysrt==1.1.2 + # via + # -r requirements/base.txt + # edxval pytest==9.0.2 # via # pytest-cov @@ -298,7 +354,7 @@ python-slugify==8.0.4 # -r requirements/base.txt # code-annotations # cookiecutter -pytz==2025.2 +pytz==2026.1.post1 # via # -r requirements/base.txt # edx-submissions @@ -319,7 +375,9 @@ regex==2026.2.28 # nltk requests==2.32.5 # via + # -r requirements/base.txt # cookiecutter + # edx-drf-extensions # xblock-sdk rich==14.3.3 # via cookiecutter @@ -331,6 +389,10 @@ scipy==1.17.1 # via # -r requirements/base.txt # chem +semantic-version==2.10.0 + # via + # -r requirements/base.txt + # edx-drf-extensions shapely==2.1.2 # via -r requirements/base.txt simplejson==3.20.2 @@ -403,6 +465,8 @@ webob==1.8.9 # -r requirements/base.txt # xblock # xblock-sdk +wrapt==2.1.1 + # via -r requirements/base.txt xblock==5.3.0 # via # -r requirements/base.txt diff --git a/test_settings.py b/test_settings.py index 3d96b5f4..67527c51 100644 --- a/test_settings.py +++ b/test_settings.py @@ -12,6 +12,7 @@ "django.contrib.auth", "django.contrib.contenttypes", "submissions", + "edxval", ] DATABASES = { @@ -87,3 +88,5 @@ def dummy_callback(_request, *_args, **_kwargs): }, }, ] + +TRANSCRIPT_LANG_CACHE_TIMEOUT = 60 * 60 * 24 # 24 hours diff --git a/xblocks_contrib/video/ajax_handler_mixin.py b/xblocks_contrib/video/ajax_handler_mixin.py new file mode 100644 index 00000000..11dfd053 --- /dev/null +++ b/xblocks_contrib/video/ajax_handler_mixin.py @@ -0,0 +1,48 @@ +""" Mixin that provides AJAX handling for Video XBlock """ +from webob import Response +from webob.multidict import MultiDict +from xblock.core import XBlock + + +class AjaxHandlerMixin: + """ + Mixin that provides AJAX handling for Video XBlock + """ + @property + def ajax_url(self): + """ + Returns the URL for the ajax handler. + """ + return self.runtime.handler_url(self, 'ajax_handler', '', '').rstrip('/?') + + @XBlock.handler + def ajax_handler(self, request, suffix=None): + """ + XBlock handler that wraps `ajax_handler` + """ + class FileObjForWebobFiles: + """ + Turn Webob cgi.FieldStorage uploaded files into pure file objects. + + Webob represents uploaded files as cgi.FieldStorage objects, which + have a .file attribute. We wrap the FieldStorage object, delegating + attribute access to the .file attribute. But the files have no + name, so we carry the FieldStorage .filename attribute as the .name. + + """ + def __init__(self, webob_file): + self.file = webob_file.file + self.name = webob_file.filename + + def __getattr__(self, name): + return getattr(self.file, name) + + # WebOb requests have multiple entries for uploaded files. handle_ajax + # expects a single entry as a list. + request_post = MultiDict(request.POST) + for key in set(request.POST.keys()): + if hasattr(request.POST[key], "file"): + request_post[key] = list(map(FileObjForWebobFiles, request.POST.getall(key))) + + response_data = self.handle_ajax(suffix, request_post) + return Response(response_data, content_type='application/json', charset='UTF-8') diff --git a/xblocks_contrib/video/assets/css/video.css b/xblocks_contrib/video/assets/css/video.css new file mode 100644 index 00000000..4b17328b --- /dev/null +++ b/xblocks_contrib/video/assets/css/video.css @@ -0,0 +1,1257 @@ +/* CSS for VideoBlock */ + +@import url("https://fonts.googleapis.com/css?family=Open+Sans:300,400,400i,600,700"); + +.xblock { + margin-bottom: calc((var(--baseline, 20px) * 1.5)); +} + +.is-hidden, +.video.closed .subtitles { + display: none; +} + +.video { + background: whitesmoke; + display: block; + margin: 0 -12px; + padding: 12px; + border-radius: 5px; + outline: none; +} + +.video:after { + content: ""; + display: table; + clear: both; +} + +.video:focus, +.video:active, +.video:hover { + border: 0; +} + +.video.is-initialized .video-wrapper .spinner { + display: none; +} + +.video.is-pre-roll .slider { + visibility: hidden; +} + +.video.is-pre-roll .video-player { + position: relative; +} + +.video.is-pre-roll .video-player::before { + display: block; + content: ""; + width: 100%; + padding-top: 55%; +} + +.video .tc-wrapper { + position: relative; +} + +.video .tc-wrapper:after { + content: ""; + display: table; + clear: both; +} + +.video .focus_grabber { + position: relative; + display: inline; + width: 0; + height: 0; +} + +.video .downloads-heading { + margin: 1em 0 0; +} + +.video .wrapper-video-bottom-section { + display: flex; + justify-content: space-between; +} + +.video .wrapper-video-bottom-section .wrapper-download-video, +.video .wrapper-video-bottom-section .wrapper-download-transcripts, +.video .wrapper-video-bottom-section .wrapper-handouts, +.video .wrapper-video-bottom-section , +.video .wrapper-video-bottom-section .wrapper-transcript-feedback { + margin-top: var(--baseline, 20px); + padding-right: var(--baseline, 20px); + vertical-align: top; +} + +@media (min-width: 768px) { + .video .wrapper-downloads { + display: flex; + } +} + +.video .wrapper-downloads .hd { + margin: 0; +} + +.video .wrapper-downloads .wrapper-download-video .video-sources { + margin: 0; +} + +.video .wrapper-downloads .wrapper-download-transcripts .list-download-transcripts { + margin: 0; + padding: 0; + list-style: none; +} + +.video .wrapper-downloads .wrapper-download-transcripts .list-download-transcripts .transcript-option { + display: flex; + align-items: center; + margin: 0; +} + +.video .wrapper-downloads .wrapper-download-transcripts .list-download-transcripts .transcript-option a.btn, +.video .wrapper-downloads .wrapper-download-transcripts .list-download-transcripts .transcript-option a.btn-link { + font-size: 16px !important; + font-weight: unset; + padding-left: 4px; +} + +.video .wrapper-downloads { + padding-right: 0; +} + +.video .wrapper-downloads .host-tag { + position: absolute; + left: -9999em; + display: inline-block; + vertical-align: middle; + color: var(--body-color, #313131); +} + +.video .wrapper-downloads .brand-logo { + display: inline-block; + max-width: 100%; + max-height: calc((var(--baseline, 20px) * 2)); + padding: calc((var(--baseline, 20px) / 4)) 0; + vertical-align: middle; +} + +.video .wrapper-transcript-feedback { + display: none; +} + +.video .wrapper-transcript-feedback .transcript-feedback-buttons { + display: flex; +} + +.video .wrapper-transcript-feedback .transcript-feedback-btn-wrapper { + margin-right: 10px; +} + +.video .wrapper-transcript-feedback .thumbs-up-btn, +.video .wrapper-transcript-feedback .thumbs-down-btn { + border: none; + box-shadow: none; + background: transparent; +} + +.video .google-disclaimer { + display: none; + margin-top: var(--baseline, 20px); + padding-right: var(--baseline, 20px); + vertical-align: top; +} + +.video .video-wrapper { + float: left; + margin-right: 2.27273%; + width: 65.90909%; + background-color: black; + position: relative; +} + +.video .video-wrapper:hover .btn-play { + color: #0075b4; +} + +.video .video-wrapper:hover .btn-play::after { + background: #fff; +} + +.video .video-wrapper .video-player-pre, +.video .video-wrapper .video-player-post { + height: 50px; + background-color: #111010; +} + +.video .video-wrapper .spinner { + transform: translate(-50%, -50%); + position: absolute; + z-index: 1; + background: rgba(0, 0, 0, 0.7); + top: 50%; + left: 50%; + padding: 30px; + border-radius: 25%; +} + +.video .video-wrapper .spinner::after { + animation: rotateCW 3s infinite linear; + content: ''; + display: block; + width: 30px; + height: 30px; + border: 7px solid white; + border-top-color: transparent; + border-radius: 100%; + position: relative; +} + +.video .video-wrapper .btn-play { + transform: translate(-50%, -50%); + position: absolute; + z-index: 1; + top: 46%; + left: 50%; + font-size: 4em; + cursor: pointer; + opacity: 0.1; +} + +.video .video-wrapper .btn-play::after { + background: var(--white, #fff); + position: absolute; + width: 50%; + height: 50%; + content: ''; + left: 0; + top: 0; + bottom: 0; + right: 0; + margin: auto; + z-index: -1; +} + +.video .video-wrapper .closed-captions { + left: 5%; + position: absolute; + width: 90%; + box-sizing: border-box; + top: 70%; + text-align: center; +} + +.video .video-wrapper .closed-captions.is-visible { + max-height: calc((var(--baseline, 20px) * 3)); + border-radius: calc((var(--baseline, 20px) / 5)); + padding: 8px calc((var(--baseline, 20px) / 2)) 8px calc((var(--baseline, 20px) * 1.5)); + background: rgba(0, 0, 0, 0.75); + color: var(--yellow, #e2c01f); +} + +.video .video-wrapper .closed-captions.is-visible::before { + position: absolute; + display: inline-block; + top: 50%; + left: var(--baseline, 20px); + margin-top: -0.6em; + font-family: 'FontAwesome'; + content: "\f142"; + color: var(--white, #fff); + opacity: 0.5; +} + +.video .video-wrapper .closed-captions.is-visible:hover, +.video .video-wrapper .closed-captions.is-visible.is-dragging { + background: black; + cursor: move; +} + +.video .video-wrapper .closed-captions.is-visible:hover::before, +.video .video-wrapper .closed-captions.is-visible.is-dragging::before { + opacity: 1; +} + +.video .video-wrapper .video-player { + overflow: hidden; + min-height: 158px; +} + +.video .video-wrapper .video-player>div { + height: 100%; +} + +.video .video-wrapper .video-player>div.hidden { + display: none; +} + +.video .video-wrapper .video-player .video-error, +.video .video-wrapper .video-player .video-hls-error { + padding: calc((var(--baseline, 20px) / 5)); + background: black; + color: white !important; +} + +.video .video-wrapper .video-player object, +.video .video-wrapper .video-player iframe, +.video .video-wrapper .video-player video { + left: 0; + display: block; + border: none; + width: 100%; +} + +.video .video-wrapper .video-player h4 { + text-align: center; + color: white; +} + +.video .video-wrapper .video-player h4.hidden { + display: none; +} + +.video .video-wrapper .video-controls { + position: relative; + border: 0; + background: #282c2e; + color: #f0f3f5; +} + +.video .video-wrapper .video-controls:after { + content: ""; + display: table; + clear: both; +} + +.video .video-wrapper .video-controls:hover ul, +.video .video-wrapper .video-controls:hover div, +.video .video-wrapper .video-controls:focus ul, +.video .video-wrapper .video-controls:focus div { + opacity: 1; +} + +.video .video-wrapper .video-controls .control { + display: inline-block; + vertical-align: middle; + margin: 0; + border: 0; + border-radius: 0; + padding: calc((var(--baseline, 20px) / 2)) calc((var(--baseline, 20px) / 1.5)); + background: #282c2e; + box-shadow: none; + text-shadow: none; + color: #cfd8dc; +} + +.video .video-wrapper .video-controls .control:hover, +.video .video-wrapper .video-controls .control:focus { + background: #171a1b; +} + +.video .video-wrapper .video-controls .control:active, +.video .video-wrapper .video-controls .is-active.control, +.video .video-wrapper .video-controls .active.control { + color: #0ea6ec; +} + +.video .video-wrapper .video-controls .control .icon { + width: 1em; +} + +.video .video-wrapper .video-controls .control .icon.icon-hd { + width: auto; +} + +.video .video-wrapper .video-controls .slider { + transform-origin: bottom left; + transition: height 0.7s ease-in-out 0s; + box-sizing: border-box; + position: absolute; + bottom: 100%; + left: 0; + right: 0; + z-index: 1; + height: calc((var(--baseline, 20px) / 4)); + margin-left: 0; + border: 1px solid #4f595d; + border-radius: 0; + background: #4f595d; +} + +.video .video-wrapper .video-controls .slider:after { + content: ""; + display: table; + clear: both; +} + +.video .video-wrapper .video-controls .slider .ui-widget-header { + background: #8e3e63; + border: 1px solid #8e3e63; + box-shadow: none; + top: -1px; + left: -1px; +} + +.video .video-wrapper .video-controls .slider .ui-corner-all.slider-range { + opacity: 0.3; + background-color: #1e91d3; +} + +.video .video-wrapper .video-controls .slider .ui-slider-handle { + transform-origin: bottom left; + transition: all 0.7s ease-in-out 0s; + box-sizing: border-box; + top: -1px; + height: calc((var(--baseline, 20px) / 4)); + width: calc((var(--baseline, 20px) / 4)); + margin-left: calc(-1 * (var(--baseline, 20px) / 8)); + border: 1px solid #cb598d; + border-radius: calc((var(--baseline, 20px) / 5)); + padding: 0; + background: #cb598d; + box-shadow: none; +} + +.video .video-wrapper .video-controls .slider .ui-slider-handle:focus, +.video .video-wrapper .video-controls .slider .ui-slider-handle:hover { + background-color: #db8baf; + border-color: #db8baf; +} + +.video .video-wrapper .video-controls .vcr { + float: left; + list-style: none; + border-right: 1px solid #282c2e; + padding: 0; +} + +@media (max-width: 1120px) { + .video .video-wrapper .video-controls .vcr { + margin-right: lh(0.5); + font-size: 0.875em; + } +} + +.video .video-wrapper .video-controls .vcr .video_control:focus { + position: relative; +} + +.video .video-wrapper .video-controls .vcr .video_control.skip { + white-space: nowrap; +} + +.video .video-wrapper .video-controls .vcr .vidtime { + padding-left: lh(0.75); + display: inline-block; + color: #cfd8dc; + -webkit-font-smoothing: antialiased; +} + +@media (max-width: 1120px) { + .video .video-wrapper .video-controls .vcr .vidtime { + padding-left: lh(0.5); + } +} + +.video .video-wrapper .video-controls .secondary-controls { + float: right; + border-left: 1px dotted #4f595d; +} + +.video .video-wrapper .video-controls .secondary-controls .volume, +.video .video-wrapper .video-controls .secondary-controls .add-fullscreen, +.video .video-wrapper .video-controls .secondary-controls .grouped-controls, +.video .video-wrapper .video-controls .secondary-controls .auto-advance, +.video .video-wrapper .video-controls .secondary-controls .quality-control { + border-left: 1px dotted #4f595d; +} + +.video .video-wrapper .video-controls .secondary-controls .speed-button:focus, +.video .video-wrapper .video-controls .secondary-controls .volume>.control:focus, +.video .video-wrapper .video-controls .secondary-controls .add-fullscreen:focus, +.video .video-wrapper .video-controls .secondary-controls .auto-advance:focus, +.video .video-wrapper .video-controls .secondary-controls .quality-control:focus, +.video .video-wrapper .video-controls .secondary-controls .toggle-transcript:focus { + position: relative; +} + +.video .video-wrapper .video-controls .secondary-controls .menu-container { + position: relative; +} + +.video .video-wrapper .video-controls .secondary-controls .menu-container .menu { + transition: none; + position: absolute; + display: none; + bottom: 100%; + right: 0; + width: 120px; + margin: 0; + border: none; + padding: 0; + box-shadow: none; + background-color: #282c2e; + list-style: none; +} + +.video .video-wrapper .video-controls .secondary-controls .menu-container .menu li { + color: #e7ecee; +} + +.video .video-wrapper .video-controls .secondary-controls .menu-container .menu li .speed-option, +.video .video-wrapper .video-controls .secondary-controls .menu-container .menu li .control-lang { + text-align: left; + display: block; + width: 100%; + border: 0; + border-radius: 0; + padding: lh(0.5); + background: #282c2e; + box-shadow: none; + color: #e7ecee; + overflow: hidden; + text-shadow: none; + text-overflow: ellipsis; + white-space: nowrap; +} + +.video .video-wrapper .video-controls .secondary-controls .menu-container .menu li .speed-option:hover, +.video .video-wrapper .video-controls .secondary-controls .menu-container .menu li .speed-option:focus, +.video .video-wrapper .video-controls .secondary-controls .menu-container .menu li .control-lang:hover, +.video .video-wrapper .video-controls .secondary-controls .menu-container .menu li .control-lang:focus { + background-color: #4f595d; + color: #fcfcfc; +} + +.video .video-wrapper .video-controls .secondary-controls .menu-container .menu li.is-active .speed-option, +.video .video-wrapper .video-controls .secondary-controls .menu-container .menu li.is-active .control-lang { + border-left: calc(var(--baseline, 20px) / 10) solid #90d7f9; + font-weight: var(--font-bold, 700); + color: #90d7f9; +} + +.video .video-wrapper .video-controls .secondary-controls .menu-container.is-opened .menu { + display: block; +} + +.video .video-wrapper .video-controls .secondary-controls .speeds, +.video .video-wrapper .video-controls .secondary-controls .lang, +.video .video-wrapper .video-controls .secondary-controls .grouped-controls { + display: inline-block; +} + +.video .video-wrapper .video-controls .secondary-controls .speeds.is-opened .control .icon { + transform: rotate(-90deg); +} + +.video .video-wrapper .video-controls .secondary-controls .speeds .speed-button .label { + padding: 0 calc((var(--baseline, 20px) / 3)) 0 0; + font-family: var(--font-family-sans-serif); + color: #e7ecee; +} + +@media (max-width: 1120px) { + .video .video-wrapper .video-controls .secondary-controls .speeds .speed-button .label { + position: absolute; + clip: rect(1px, 1px, 1px, 1px); + } +} + +.video .video-wrapper .video-controls .secondary-controls .speeds .speed-button .value { + padding: 0 lh(0.5) 0 0; + color: #e7ecee; + font-weight: bold; +} + +@media (max-width: 1120px) { + .video .video-wrapper .video-controls .secondary-controls .speeds .speed-button .value { + padding: 0 lh(0.5); + } +} + +.video .video-wrapper .video-controls .secondary-controls .lang .language-menu { + width: var(--baseline, 20px); + padding: calc((var(--baseline, 20px) / 2)) 0; +} + +.video .video-wrapper .video-controls .secondary-controls .lang.is-opened .control .icon { + transform: rotate(90deg); +} + +.video .video-wrapper .video-controls .secondary-controls .volume { + display: inline-block; + position: relative; +} + +.video .video-wrapper .video-controls .secondary-controls .volume.is-opened .volume-slider-container { + display: block; + opacity: 1; +} + +.video .video-wrapper .video-controls .secondary-controls .volume:not(:first-child)>a { + border-left: none; +} + +.video .video-wrapper .video-controls .secondary-controls .volume .volume-slider-container { + transition: none; + display: none; + position: absolute; + bottom: 100%; + right: 0; + width: 41px; + height: 120px; + background-color: #282c2e; +} + +.video .video-wrapper .video-controls .secondary-controls .volume .volume-slider-container .volume-slider { + height: 100px; + width: calc((var(--baseline, 20px) / 4)); + margin: 14px auto; + box-sizing: border-box; + border: 1px solid #4f595d; + background: #4f595d; +} + +.video .video-wrapper .video-controls .secondary-controls .volume .volume-slider-container .volume-slider .ui-slider-handle { + transition: height var(--tmg-s2, 2s) ease-in-out 0s, width var(--tmg-s2, 2s) ease-in-out 0s; + left: -5px; + box-sizing: border-box; + height: 13px; + width: 13px; + border: 1px solid #cb598d; + border-radius: calc((var(--baseline, 20px) / 5)); + padding: 0; + background: #cb598d; + box-shadow: none; +} + +.video .video-wrapper .video-controls .secondary-controls .volume .volume-slider-container .volume-slider .ui-slider-handle:hover, +.video .video-wrapper .video-controls .secondary-controls .volume .volume-slider-container .volume-slider .ui-slider-handle:focus { + background: #db8baf; + border-color: #db8baf; +} + +.video .video-wrapper .video-controls .secondary-controls .volume .volume-slider-container .volume-slider .ui-slider-range { + background: #8e3e63; + border: 1px solid #8e3e63; + left: -1px; + bottom: -1px; +} + +.video .video-wrapper .video-controls .secondary-controls .quality-control { + font-weight: 700; + letter-spacing: -1px; +} + +.video .video-wrapper .video-controls .secondary-controls .quality-control.active { + color: #0ea6ec; +} + +.video .video-wrapper .video-controls .secondary-controls .quality-control.is-hidden, +.video.closed .video-wrapper .video-controls .secondary-controls .quality-control.subtitles { + display: none !important; +} + +.video .video-wrapper .video-controls .secondary-controls .toggle-transcript.is-active { + color: #0ea6ec; +} + +.video .video-wrapper .video-controls .secondary-controls .lang>.hide-subtitles { + transition: none; +} + +.video .video-wrapper:hover .video-controls .slider { + height: calc((var(--baseline, 20px) / 1.5)); +} + +.video .video-wrapper:hover .video-controls .slider .ui-slider-handle { + height: calc((var(--baseline, 20px) / 1.5)); + width: calc((var(--baseline, 20px) / 1.5)); +} + +.video.video-fullscreen .closed-captions { + width: 65%; +} + +.video.video-fullscreen.closed .closed-captions { + width: 90%; +} + +.video .subtitles { + float: left; + overflow: auto; + max-height: 460px; + width: 31.81818%; + padding: 0; + font-size: 14px; + visibility: visible; +} + +.video .subtitles a { + color: #0074b5; +} + +.video .subtitles .subtitles-menu { + height: 100%; + margin: 0; + padding: 0 3px; + list-style: none; +} + +.video .subtitles .subtitles-menu li { + margin-bottom: 8px; + border: 0; + padding: 0; + color: #0074b5; + line-height: lh(); +} + +.video .subtitles .subtitles-menu li:has(> span:empty) { + display: none; +} + +.video .subtitles .subtitles-menu li span { + display: block; +} + +.video .subtitles .subtitles-menu li.current { + color: #333; + font-weight: 700; +} + +.video .subtitles .subtitles-menu li.focused { + outline: #000 dotted thin; + outline-offset: -1px; +} + +.video .subtitles .subtitles-menu li:hover, +.video .subtitles .subtitles-menu li:focus { + text-decoration: underline; +} + +.video .subtitles .subtitles-menu li:empty { + margin-bottom: 0; +} + +.video .subtitles .subtitles-menu li.spacing:last-of-type { + position: relative; +} + +.video .subtitles .subtitles-menu li.spacing:last-of-type .transcript-end { + position: absolute; + bottom: 0; +} + +.video.closed .video-wrapper { + width: 100%; + background-color: inherit; +} + +.video.closed .video-wrapper .video-controls.html5 { + bottom: 0; + left: 0; + right: 0; + position: absolute; + z-index: 1; +} + +.video.closed .video-wrapper .video-player-pre, +.video.closed .video-wrapper .video-player-post { + height: 0; +} + +.video.closed .video-wrapper .video-player h3 { + color: black; +} + +.video.closed .subtitles.html5 { + background-color: rgba(243, 243, 243, 0.8); + height: 100%; + position: absolute; + right: 0; + bottom: 0; + top: 0; + width: 275px; + padding: 0 var(--baseline, 20px); + display: none; +} + +.video.video-fullscreen { + background: rgba(0, 0, 0, 0.95); + border: 0; + bottom: 0; + height: 100%; + left: 0; + margin: 0; + padding: 0; + position: fixed; + top: 0; + width: 100%; + vertical-align: middle; + border-radius: 0; +} + +.video.video-fullscreen.closed .tc-wrapper .video-wrapper { + width: 100%; +} + +.video.video-fullscreen .video-wrapper .video-player-pre, +.video.video-fullscreen .video-wrapper .video-player-post { + height: 0; +} + +.video.video-fullscreen .video-wrapper { + position: static; +} + +.video.video-fullscreen .video-wrapper .video-player h3 { + color: white; +} + +.video.video-fullscreen .tc-wrapper { + width: 100%; + height: 100%; + position: static; +} + +.video.video-fullscreen .tc-wrapper:after { + content: ""; + display: table; + clear: both; +} + +.video.video-fullscreen .tc-wrapper .video-wrapper { + height: 100%; + width: 75%; + margin-right: 0; + vertical-align: middle; +} + +.video.video-fullscreen .tc-wrapper .video-wrapper object, +.video.video-fullscreen .tc-wrapper .video-wrapper iframe, +.video.video-fullscreen .tc-wrapper .video-wrapper video { + position: absolute; + width: auto; + height: auto; +} + +.video.video-fullscreen .tc-wrapper .video-controls { + position: absolute; + bottom: 0; + left: 0; + width: 100%; +} + +.video.video-fullscreen .subtitles { + height: 100%; + width: 25%; + padding: lh(); + box-sizing: border-box; + transition: none; + background: var(--black, #000); + visibility: visible; +} + +.video.video-fullscreen .subtitles li { + color: #aaa; +} + +.video.video-fullscreen .subtitles li.current { + color: var(--white, #fff); +} + +.video.is-touch .tc-wrapper .video-wrapper object, +.video.is-touch .tc-wrapper .video-wrapper iframe, +.video.is-touch .tc-wrapper .video-wrapper video { + width: 100%; + height: 100%; +} + +.video .video-pre-roll { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-position: 50% 50%; + background-repeat: no-repeat; + background-size: 100%; + background-color: var(--black, #000); +} + +.video .video-pre-roll.is-html5 { + background-size: 15%; +} + +.video .video-pre-roll .btn-play.btn-pre-roll { + padding: var(--baseline, 20px); + border: none; + border-radius: var(--baseline, 20px); + background: var(--black-t2, rgba(0, 0, 0, 0.5)); + box-shadow: none; +} + +.video .video-pre-roll .btn-play.btn-pre-roll::after { + display: none; +} + +.video .video-pre-roll .btn-play.btn-pre-roll img { + height: calc((var(--baseline, 20px) * 4)); + width: calc((var(--baseline, 20px) * 4)); +} + +.video .video-pre-roll .btn-play.btn-pre-roll:hover, +.video .video-pre-roll .btn-play.btn-pre-roll:focus { + background: var(--blue, #0075b4); +} + +.video .video-wrapper .video-controls .slider .ui-slider-handle, +.video .video-wrapper .video-controls .secondary-controls .menu-container .menu li, +.video .video-wrapper .video-controls .secondary-controls .volume .volume-slider-container .volume-slider .ui-slider-handle, +.video .subtitles .subtitles-menu li, +.a11y-menu-container .a11y-menu-list li { + cursor: pointer; +} + +.video.closed .subtitles.html5 { + z-index: 0; +} + +.video .video-wrapper .video-controls .secondary-controls .menu-container .menu, +.video .video-wrapper .video-controls .secondary-controls .volume .volume-slider-container { + z-index: 10; +} + +.video .video-pre-roll, +.a11y-menu-container .a11y-menu-list { + z-index: 1000; +} + +.video.video-fullscreen, +.video.video-fullscreen .tc-wrapper .video-controls, +.overlay { + z-index: 10000; +} + +.contextmenu, +.submenu { + z-index: 100000; +} + +.video-tracks .a11y-menu-container>a::after { + font-family: FontAwesome; + -webkit-font-smoothing: antialiased; + display: inline-block; + speak: none; +} + +.a11y-menu-container { + position: relative; +} + +.a11y-menu-container.open .a11y-menu-list { + display: block; +} + +.a11y-menu-container .a11y-menu-list { + top: 100%; + margin: 0; + padding: 0; + display: none; + position: absolute; + list-style: none; + background-color: var(--white, #fff); + border: 1px solid #eee; +} + +.a11y-menu-container .a11y-menu-list li { + margin: 0; + padding: 0; + border-bottom: 1px solid #eee; + color: var(--white, #fff); +} + +.a11y-menu-container .a11y-menu-list li a { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--gray-l2, #adadad); + font-size: 14px; + line-height: 23px; +} + +.a11y-menu-container .a11y-menu-list li a:hover, +.a11y-menu-container .a11y-menu-list li a:focus { + color: var(--gray-d1, #5e5e5e); +} + +.a11y-menu-container .a11y-menu-list li.active a { + color: #009fe6; +} + +.a11y-menu-container .a11y-menu-list li:last-child { + box-shadow: none; + border-bottom: 0; + margin-top: 0; +} + +.video-tracks .a11y-menu-container { + display: inline-block; + vertical-align: top; + border-left: 1px solid #eee; +} + +.video-tracks .a11y-menu-container.open>a { + background-color: var(--action-primary-active-bg, #0075b4); + color: var(--very-light-text, white); +} + +.video-tracks .a11y-menu-container.open>a::after { + color: var(--very-light-text, white); +} + +.video-tracks .a11y-menu-container>a { + transition: all var(--tmg-f2, 0.25s) ease-in-out 0s; + font-size: 12px; + display: block; + border-radius: 0 3px 3px 0; + background-color: var(--very-light-text, white); + padding: calc((var(--baseline, 20px) * 0.75)) calc((var(--baseline, 20px) * 1.25)) calc((var(--baseline, 20px) * 0.75)) calc((var(--baseline, 20px) * 0.75)); + color: var(--gray-l2, #adadad); + min-width: 1.5em; + line-height: 14px; + text-align: center; + overflow: hidden; + text-overflow: ellipsis; +} + +.video-tracks .a11y-menu-container>a::after { + content: "\f0d7"; + position: absolute; + right: calc((var(--baseline, 20px) * 0.5)); + top: 33%; + color: var(--lighter-base-font-color, #646464); +} + +.video-tracks .a11y-menu-container .a11y-menu-list { + right: 0; +} + +.video-tracks .a11y-menu-container .a11y-menu-list li { + font-size: 0.875em; +} + +.video-tracks .a11y-menu-container .a11y-menu-list li a { + border: 0; + display: block; + padding: 0.70788em; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.contextmenu, +.submenu { + border: 1px solid #333; + background: var(--white, #fff); + color: #333; + padding: 0; + margin: 0; + list-style: none; + position: absolute; + top: 0; + display: none; + outline: none; + cursor: default; + white-space: nowrap; +} + +.contextmenu.is-opened, +.submenu.is-opened { + display: block; +} + +.contextmenu .menu-item, +.contextmenu .submenu-item, +.submenu .menu-item, +.submenu .submenu-item { + border-top: 1px solid var(--gray-l3, #c8c8c8); + padding: calc((var(--baseline, 20px) / 4)) calc((var(--baseline, 20px) / 2)); + outline: none; +} + +.contextmenu .menu-item>span, +.contextmenu .submenu-item>span, +.submenu .menu-item>span, +.submenu .submenu-item>span { + color: #333; +} + +.contextmenu .menu-item:first-child, +.contextmenu .submenu-item:first-child, +.submenu .menu-item:first-child, +.submenu .submenu-item:first-child { + border-top: none; +} + +.contextmenu .menu-item:focus, +.contextmenu .submenu-item:focus, +.submenu .menu-item:focus, +.submenu .submenu-item:focus { + background: #333; + color: var(--white, #fff); +} + +.contextmenu .menu-item:focus>span, +.contextmenu .submenu-item:focus>span, +.submenu .menu-item:focus>span, +.submenu .submenu-item:focus>span { + color: var(--white, #fff); +} + +.contextmenu .submenu-item, +.submenu .submenu-item { + position: relative; + padding: calc((var(--baseline, 20px) / 4)) var(--baseline, 20px) calc((var(--baseline, 20px) / 4)) calc((var(--baseline, 20px) / 2)); +} + +.contextmenu .submenu-item::after, +.submenu .submenu-item::after { + content: '\25B6'; + position: absolute; + right: 5px; + line-height: 25px; + font-size: 10px; +} + +.contextmenu .submenu-item .submenu, +.submenu .submenu-item .submenu { + display: none; +} + +.contextmenu .submenu-item.is-opened, +.submenu .submenu-item.is-opened { + background: #333; + color: var(--white, #fff); +} + +.contextmenu .submenu-item.is-opened>span, +.submenu .submenu-item.is-opened>span { + color: var(--white, #fff); +} + +.contextmenu .submenu-item.is-opened>.submenu, +.submenu .submenu-item.is-opened>.submenu { + display: block; +} + +.contextmenu .submenu-item .is-selected, +.submenu .submenu-item .is-selected { + font-weight: bold; +} + +.contextmenu .is-disabled, +.submenu .is-disabled { + pointer-events: none; + color: var(--gray-l3, #c8c8c8); +} + +.overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: transparent; +} + +.wrapper-social-share .social-toggle-btn { + background: var(--primary); + font-size: 13px; + font-weight: 700; + padding: calc(var(--baseline) * 0.35) calc(var(--baseline) * 0.9); + color: var(--white); + box-shadow: none; + text-shadow: none; + border-radius: 3px; + border: none; +} + +.wrapper-social-share .social-toggle-btn:hover, +.wrapper-social-share .social-toggle-btn:focus { + background: var(--btn-brand-focus-background); +} + +.wrapper-social-share .social-toggle-btn .fa { + margin-right: calc(var(--baseline) * 0.4); +} + +.wrapper-social-share .container-social-share { + padding: calc(var(--baseline) * 0.4); + width: 300px; + border-radius: 6px; + background-color: var(--white); + box-shadow: rgba(0, 0, 0, 0.15) 0 0.5rem 1rem, rgba(0, 0, 0, 0.15) 0 0.25rem 0.625rem; +} + +.wrapper-social-share .container-social-share .close-btn { + float: right; + cursor: pointer; + vertical-align: top; + display: inline-flex; + color: var(--black); + text-decoration: none !important; +} + +.wrapper-social-share .container-social-share .social-share-link { + margin-right: calc(var(--baseline) * 0.2); + font-size: 24px; + height: 24px; + vertical-align: middle; + text-decoration: none; + display: inline-flex; +} + +.wrapper-social-share .container-social-share .social-share-link > span > svg { + width: auto; + height: 24px; + vertical-align: top; + display: inline-flex; +} + +.wrapper-social-share .container-social-share .public-video-url-container { + padding: calc(var(--baseline) * 0.4); + display: flex; + align-items: center; + justify-content: space-between; + background-color: #f2f0ef; +} + +.wrapper-social-share .container-social-share .public-video-url-link { + color: var(--black); + overflow: hidden; + text-overflow: ellipsis; + vertical-align: middle; + white-space: nowrap; +} + +.wrapper-social-share .container-social-share .public-video-url-link:hover { + text-decoration: underline; +} + +.wrapper-social-share .container-social-share .public-video-copy-btn { + margin-left: calc(var(--baseline) * 0.7); + flex-shrink: 0; + color: var(--primary); + cursor: pointer; +} + +.wrapper-social-share .container-social-share .public-video-copy-btn:hover { + text-decoration: none; + color: var(--link-hover-color); +} diff --git a/xblocks_contrib/video/assets/jasmine.common.conf.js b/xblocks_contrib/video/assets/jasmine.common.conf.js new file mode 100644 index 00000000..9001185e --- /dev/null +++ b/xblocks_contrib/video/assets/jasmine.common.conf.js @@ -0,0 +1,15 @@ +/* eslint-env node */ + +'use strict'; + +// By default, fixtures are loaded from spec/javascripts/fixtures but in karma everything gets served from /base +jasmine.getFixtures().fixturesPath = '/base/tests/'; + +// Allow more time for video/async tests (e.g. HTML5 init, ready) +jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000; + +// https://github.com/edx/js-test-tool/blob/master/js_test_tool/templates/jasmine_test_runner.html#L10 +// Stub out modal dialog alerts, which will prevent +// us from accessing the test results in the DOM +window.confirm = function() { return true; }; +window.alert = function() { }; diff --git a/xblocks_contrib/video/assets/jasmine_stack_trace.js b/xblocks_contrib/video/assets/jasmine_stack_trace.js new file mode 100644 index 00000000..f61d591f --- /dev/null +++ b/xblocks_contrib/video/assets/jasmine_stack_trace.js @@ -0,0 +1,29 @@ +/* This file overrides ExceptionFormatter of jasmine before it's initialization in karma-jasmine's + boot.js. It's important because ExceptionFormatter returns a constructor function. Once the method has been + initialized we can't override the ExceptionFormatter as Jasmine then uses the stored reference to the function */ +(function() { + /* globals jasmineRequire */ + + 'use strict'; + + var OldExceptionFormatter = jasmineRequire.ExceptionFormatter(), + oldExceptionFormatter = new OldExceptionFormatter(), + MAX_STACK_TRACE_LINES = 10; + + jasmineRequire.ExceptionFormatter = function() { + function ExceptionFormatter() { + this.message = oldExceptionFormatter.message; + this.stack = function(error) { + var errorMsg = null; + + if (error) { + errorMsg = error.stack.split('\n').slice(0, MAX_STACK_TRACE_LINES).join('\n'); + } + + return errorMsg; + }; + } + + return ExceptionFormatter; + }; +}()); diff --git a/xblocks_contrib/video/assets/js/src/00_async_process.js b/xblocks_contrib/video/assets/js/src/00_async_process.js new file mode 100644 index 00000000..a909e822 --- /dev/null +++ b/xblocks_contrib/video/assets/js/src/00_async_process.js @@ -0,0 +1,52 @@ +'use strict'; + +/** + * Provides convenient way to process big amount of data without UI blocking. + * + * @param {array} list Array to process. + * @param {function} process Calls this function on each item in the list. + * @return {array} Returns a Promise object to observe when all actions of a + * certain type bound to the collection, queued or not, have finished. + */ +let AsyncProcess = { + array: function(list, process) { + if (!_.isArray(list)) { + return $.Deferred().reject().promise(); + } + + if (!_.isFunction(process) || !list.length) { + return $.Deferred().resolve(list).promise(); + } + + let MAX_DELAY = 50, // maximum amount of time that js code should be allowed to run continuously + dfd = $.Deferred(); + let result = []; + let index = 0; + let len = list.length; + + let getCurrentTime = function() { + return (new Date()).getTime(); + }; + + let handler = function() { + let start = getCurrentTime(); + + do { + result[index] = process(list[index], index); + index++; + } while (index < len && getCurrentTime() - start < MAX_DELAY); + + if (index < len) { + setTimeout(handler, 25); + } else { + dfd.resolve(result); + } + }; + + setTimeout(handler, 25); + + return dfd.promise(); + } +}; + +export default AsyncProcess; diff --git a/xblocks_contrib/video/assets/js/src/00_component.js b/xblocks_contrib/video/assets/js/src/00_component.js new file mode 100644 index 00000000..2ac183b1 --- /dev/null +++ b/xblocks_contrib/video/assets/js/src/00_component.js @@ -0,0 +1,81 @@ +'use strict'; + +import _ from 'underscore'; + + +/** + * Creates a new object with the specified prototype object and properties. + * @param {Object} o The object which should be the prototype of the + * newly-created object. + * @private + * @throws {TypeError, Error} + * @return {Object} + */ +let inherit = Object.create || (function() { + let F = function() {}; + + return function(o) { + if (arguments.length > 1) { + throw Error('Second argument not supported'); + } + if (_.isNull(o) || _.isUndefined(o)) { + throw Error('Cannot set a null [[Prototype]]'); + } + if (!_.isObject(o)) { + throw TypeError('Argument must be an object'); + } + + F.prototype = o; + + return new F(); + }; +}()); + +/** + * Component module. + * @exports video/00_component.js + * @constructor + * @return {jquery Promise} + */ +let Component = function() { + if ($.isFunction(this.initialize)) { + // eslint-disable-next-line prefer-spread + return this.initialize.apply(this, arguments); + } +}; + +/** + * Returns new constructor that inherits form the current constructor. + * @static + * @param {Object} protoProps The object containing which will be added to + * the prototype. + * @return {Object} + */ +Component.extend = function(protoProps, staticProps) { + let Parent = this; + let Child = function() { + if ($.isFunction(this.initialize)) { + // eslint-disable-next-line prefer-spread + return this.initialize.apply(this, arguments); + } + }; + + // Inherit methods and properties from the Parent prototype. + Child.prototype = inherit(Parent.prototype); + Child.constructor = Parent; + // Provide access to parent's methods and properties + Child.__super__ = Parent.prototype; + + // Extends inherited methods and properties by methods/properties + // passed as argument. + if (protoProps) { + $.extend(Child.prototype, protoProps); + } + + // Inherit static methods and properties + $.extend(Child, Parent, staticProps); + + return Child; +}; + +export default Component; diff --git a/xblocks_contrib/video/assets/js/src/00_i18n.js b/xblocks_contrib/video/assets/js/src/00_i18n.js new file mode 100644 index 00000000..1962ed4e --- /dev/null +++ b/xblocks_contrib/video/assets/js/src/00_i18n.js @@ -0,0 +1,35 @@ +'use strict'; + +/** + * i18n module. + * @exports video/00_i18n.js + * @return {object} + */ + +let i18n = { + Play: gettext('Play'), + Pause: gettext('Pause'), + Mute: gettext('Mute'), + Unmute: gettext('Unmute'), + 'Exit full browser': gettext('Exit full browser'), + 'Fill browser': gettext('Fill browser'), + Speed: gettext('Speed'), + 'Auto-advance': gettext('Auto-advance'), + Volume: gettext('Volume'), + // Translators: Volume level equals 0%. + Muted: gettext('Muted'), + // Translators: Volume level in range ]0,20]% + 'Very low': gettext('Very low'), + // Translators: Volume level in range ]20,40]% + Low: gettext('Low'), + // Translators: Volume level in range ]40,60]% + Average: gettext('Average'), + // Translators: Volume level in range ]60,80]% + Loud: gettext('Loud'), + // Translators: Volume level in range ]80,99]% + 'Very loud': gettext('Very loud'), + // Translators: Volume level equals 100%. + Maximum: gettext('Maximum') +}; + +export default i18n; diff --git a/xblocks_contrib/video/assets/js/src/00_iterator.js b/xblocks_contrib/video/assets/js/src/00_iterator.js new file mode 100644 index 00000000..5b597f20 --- /dev/null +++ b/xblocks_contrib/video/assets/js/src/00_iterator.js @@ -0,0 +1,83 @@ +'use strict'; + +/** + * Provides convenient way to work with iterable data. + * @exports video/00_iterator.js + * @constructor + * @param {array} list Array to be iterated. + */ +let Iterator = function(list) { + this.list = list; + this.index = 0; + this.size = this.list.length; + this.lastIndex = this.list.length - 1; +}; + +Iterator.prototype = { + + /** + * Checks validity of provided index for the iterator. + * @access protected + * @param {numebr} index + * @return {boolean} + */ + _isValid: function(index) { + return _.isNumber(index) && index < this.size && index >= 0; + }, + + /** + * Returns next element. + * @param {number} [index] Updates current position. + * @return {any} + */ + next: function(index) { + if (!(this._isValid(index))) { + index = this.index; + } + + this.index = (index >= this.lastIndex) ? 0 : index + 1; + + return this.list[this.index]; + }, + + /** + * Returns previous element. + * @param {number} [index] Updates current position. + * @return {any} + */ + prev: function(index) { + if (!(this._isValid(index))) { + index = this.index; + } + + this.index = (index < 1) ? this.lastIndex : index - 1; + + return this.list[this.index]; + }, + + /** + * Returns last element in the list. + * @return {any} + */ + last: function() { + return this.list[this.lastIndex]; + }, + + /** + * Returns first element in the list. + * @return {any} + */ + first: function() { + return this.list[0]; + }, + + /** + * Returns `true` if current position is last for the iterator. + * @return {boolean} + */ + isEnd: function() { + return this.index === this.lastIndex; + } +}; + +export default Iterator; diff --git a/xblocks_contrib/video/assets/js/src/00_resizer.js b/xblocks_contrib/video/assets/js/src/00_resizer.js new file mode 100644 index 00000000..d892ec4d --- /dev/null +++ b/xblocks_contrib/video/assets/js/src/00_resizer.js @@ -0,0 +1,236 @@ +'use strict'; + +import _ from 'underscore'; + + +let Resizer = function(params) { + let defaults = { + container: window, + element: null, + containerRatio: null, + elementRatio: null + }, + callbacksList = [], + delta = { + height: 0, + width: 0 + }, + module = {}; + let mode = null, + config; + + // eslint-disable-next-line no-shadow + let initialize = function(params) { + if (!config) { + config = defaults; + } + + config = $.extend(true, {}, config, params); + + if (!config.element) { + console.log( + 'Required parameter `element` is not passed.' + ); + } + + return module; + }; + + let getData = function() { + let $container = $(config.container), + containerWidth = $container.width() + delta.width, + containerHeight = $container.height() + delta.height; + let containerRatio = config.containerRatio; + + let $element = $(config.element); + let elementRatio = config.elementRatio; + + if (!containerRatio) { + containerRatio = containerWidth / containerHeight; + } + + if (!elementRatio) { + elementRatio = $element.width() / $element.height(); + } + + return { + containerWidth: containerWidth, + containerHeight: containerHeight, + containerRatio: containerRatio, + element: $element, + elementRatio: elementRatio + }; + }; + + let align = function() { + let data = getData(); + + switch (mode) { + case 'height': + alignByHeightOnly(); + break; + + case 'width': + alignByWidthOnly(); + break; + + default: + if (data.containerRatio >= data.elementRatio) { + alignByHeightOnly(); + } else { + alignByWidthOnly(); + } + break; + } + + fireCallbacks(); + + return module; + }; + + let alignByWidthOnly = function() { + let data = getData(), + height = data.containerWidth / data.elementRatio; + + data.element.css({ + height: height, + width: data.containerWidth, + top: 0.5 * (data.containerHeight - height), + left: 0 + }); + + return module; + }; + + let alignByHeightOnly = function() { + let data = getData(), + width = data.containerHeight * data.elementRatio; + + data.element.css({ + height: data.containerHeight, + width: data.containerHeight * data.elementRatio, + top: 0, + left: 0.5 * (data.containerWidth - width) + }); + + return module; + }; + + let setMode = function(param) { + if (_.isString(param)) { + mode = param; + align(); + } + + return module; + }; + + let setElement = function(element) { + config.element = element; + + return module; + }; + + let addCallback = function(func) { + if ($.isFunction(func)) { + callbacksList.push(func); + } else { + console.error('[Video info]: TypeError: Argument is not a function.'); + } + + return module; + }; + + let addOnceCallback = function(func) { + if ($.isFunction(func)) { + let decorator = function() { + func(); + removeCallback(func); + }; + + addCallback(decorator); + } else { + console.error('TypeError: Argument is not a function.'); + } + + return module; + }; + + let fireCallbacks = function() { + $.each(callbacksList, function(index, callback) { + callback(); + }); + }; + + let removeCallbacks = function() { + callbacksList.length = 0; + + return module; + }; + + let removeCallback = function(func) { + let index = $.inArray(func, callbacksList); + + if (index !== -1) { + return callbacksList.splice(index, 1); + } + }; + + let resetDelta = function() { + // eslint-disable-next-line no-multi-assign + delta.height = delta.width = 0; + + return module; + }; + + let addDelta = function(value, side) { + if (_.isNumber(value) && _.isNumber(delta[side])) { + delta[side] += value; + } + + return module; + }; + + let substractDelta = function(value, side) { + if (_.isNumber(value) && _.isNumber(delta[side])) { + delta[side] -= value; + } + + return module; + }; + + let destroy = function() { + let data = getData(); + data.element.css({ + height: '', width: '', top: '', left: '' + }); + removeCallbacks(); + resetDelta(); + mode = null; + }; + + initialize.apply(module, arguments); + + return $.extend(true, module, { + align: align, + alignByWidthOnly: alignByWidthOnly, + alignByHeightOnly: alignByHeightOnly, + destroy: destroy, + setParams: initialize, + setMode: setMode, + setElement: setElement, + callbacks: { + add: addCallback, + once: addOnceCallback, + remove: removeCallback, + removeAll: removeCallbacks + }, + delta: { + add: addDelta, + substract: substractDelta, + reset: resetDelta + } + }); +}; + +export default Resizer; diff --git a/xblocks_contrib/video/assets/js/src/00_sjson.js b/xblocks_contrib/video/assets/js/src/00_sjson.js new file mode 100644 index 00000000..99d870ff --- /dev/null +++ b/xblocks_contrib/video/assets/js/src/00_sjson.js @@ -0,0 +1,108 @@ +'use strict'; + +let Sjson = function(data) { + let sjson = { + start: data.start.concat(), + text: data.text.concat() + }, + module = {}; + + let getter = function(propertyName) { + return function() { + return sjson[propertyName]; + }; + }; + + let getStartTimes = getter('start'); + + let getCaptions = getter('text'); + + let size = function() { + return sjson.text.length; + }; + + function search(time, startTime, endTime) { + let start = getStartTimes(), + max = size() - 1, + min = 0, + results, + index; + + // if we specify a start and end time to search, + // search the filtered list of captions in between + // the start / end times. + // Else, search the unfiltered list. + if (typeof startTime !== 'undefined' + && typeof endTime !== 'undefined') { + results = filter(startTime, endTime); + start = results.start; + max = results.captions.length - 1; + } else { + start = getStartTimes(); + } + while (min < max) { + index = Math.ceil((max + min) / 2); + + if (time < start[index]) { + max = index - 1; + } + + if (time >= start[index]) { + min = index; + } + } + + return min; + } + + function filter(start, end) { + /* filters captions that occur between inputs + * `start` and `end`. Start and end should + * be Numbers (doubles) corresponding to the + * number of seconds elapsed since the beginning + * of the video. + * + * Returns an object with properties + * "start" and "captions" representing + * parallel arrays of start times and + * their corresponding captions. + */ + let filteredTimes = []; + let filteredCaptions = []; + let startTimes = getStartTimes(); + let captions = getCaptions(); + + if (startTimes.length !== captions.length) { + console.warn('video caption and start time arrays do not match in length'); + } + + // if end is null, then it's been set to + // some erroneous value, so filter using the + // entire array as long as it's not empty + if (end === null && startTimes.length) { + end = startTimes[startTimes.length - 1]; + } + + _.filter(startTimes, function(currentStartTime, i) { + if (currentStartTime >= start && currentStartTime <= end) { + filteredTimes.push(currentStartTime); + filteredCaptions.push(captions[i]); + } + }); + + return { + start: filteredTimes, + captions: filteredCaptions + }; + } + + return { + getCaptions: getCaptions, + getStartTimes: getStartTimes, + getSize: size, + filter: filter, + search: search + }; +}; + +export default Sjson; diff --git a/xblocks_contrib/video/assets/js/src/00_video_storage.js b/xblocks_contrib/video/assets/js/src/00_video_storage.js new file mode 100644 index 00000000..f2293336 --- /dev/null +++ b/xblocks_contrib/video/assets/js/src/00_video_storage.js @@ -0,0 +1,96 @@ +'use strict'; + +/** + * Provides convenient way to store key value pairs. + * + * @param {string} namespace Namespace that is used to store data. + * @return {object} VideoStorage API. + */ +let VideoStorage = function(namespace, id) { + /** + * Adds new value to the storage or rewrites existent. + * + * @param {string} name Identifier of the data. + * @param {any} value Data to store. + * @param {boolean} instanceSpecific Data with this flag will be added + * to instance specific storage. + */ + let setItem = function(name, value, instanceSpecific) { + if (name) { + if (instanceSpecific) { + window[namespace][id][name] = value; + } else { + window[namespace][name] = value; + } + } + }; + + /** + * Returns the current value associated with the given name. + * + * @param {string} name Identifier of the data. + * @param {boolean} instanceSpecific Data with this flag will be added + * to instance specific storage. + * @return {any} The current value associated with the given name. + * If the given key does not exist in the list + * associated with the object then this method must return null. + */ + let getItem = function(name, instanceSpecific) { + if (instanceSpecific) { + return window[namespace][id][name]; + } else { + return window[namespace][name]; + } + }; + + /** + * Removes the current value associated with the given name. + * + * @param {string} name Identifier of the data. + * @param {boolean} instanceSpecific Data with this flag will be added + * to instance specific storage. + */ + let removeItem = function(name, instanceSpecific) { + if (instanceSpecific) { + delete window[namespace][id][name]; + } else { + delete window[namespace][name]; + } + }; + + /** + * Empties the storage. + * + */ + let clear = function() { + window[namespace] = {}; + window[namespace][id] = {}; + }; + + /** + * Initializes the module: creates a storage with proper namespace. + * + * @private + */ + (function initialize() { + if (!namespace) { + namespace = 'VideoStorage'; + } + if (!id) { + // Generate random alpha-numeric string. + id = Math.random().toString(36).slice(2); + } + + window[namespace] = window[namespace] || {}; + window[namespace][id] = window[namespace][id] || {}; + }()); + + return { + clear: clear, + getItem: getItem, + removeItem: removeItem, + setItem: setItem + }; +}; + +export default VideoStorage; diff --git a/xblocks_contrib/video/assets/js/src/01_initialize.js b/xblocks_contrib/video/assets/js/src/01_initialize.js new file mode 100644 index 00000000..85248b3f --- /dev/null +++ b/xblocks_contrib/video/assets/js/src/01_initialize.js @@ -0,0 +1,845 @@ +/* eslint-disable no-console, no-param-reassign */ +/** + * @file Initialize module works with the JSON config, and sets up various + * settings, parameters, variables. After all setup actions are performed, it + * invokes the video player to play the specified video. This module must be + * invoked first. It provides several functions which do not fit in with other + * modules. + * + * @external VideoPlayer + * + * @module Initialize + */ + +import VideoPlayer from './03_video_player.js'; +import i18n from './00_i18n.js'; +import _ from 'underscore'; +import moment from 'moment'; + +/** + * @function + * + * Initialize module exports this function. + * + * @param {object} state The object containg the state of the video player. + * All other modules, their parameters, public variables, etc. are + * available via this object. + * @param {DOM element} element Container of the entire Video DOM element. + */ +let Initialize = function(state, element) { + _makeFunctionsPublic(state); + + state.initialize(element) + .done(function() { + if (state.isYoutubeType()) { + state.parseSpeed(); + } + // On iPhones and iPods native controls are used. + if (/iP(hone|od)/i.test(state.isTouch[0])) { + _hideWaitPlaceholder(state); + state.el.trigger('initialize', arguments); + + return false; + } + + _initializeModules(state, i18n) + .done(function() { + // On iPad ready state occurs just after start playing. + // We hide controls before video starts playing. + if (/iPad|Android/i.test(state.isTouch[0])) { + state.el.on('play', _.once(function() { + state.trigger('videoControl.show', null); + })); + } else { + // On PC show controls immediately. + state.trigger('videoControl.show', null); + } + + _hideWaitPlaceholder(state); + state.el.trigger('initialize', arguments); + }); + }); +}; + +/* eslint-disable no-use-before-define */ +let methodsDict = { + bindTo: bindTo, + fetchMetadata: fetchMetadata, + getCurrentLanguage: getCurrentLanguage, + getDuration: getDuration, + getPlayerMode: getPlayerMode, + getVideoMetadata: getVideoMetadata, + initialize: initialize, + isHtml5Mode: isHtml5Mode, + isFlashMode: isFlashMode, + isYoutubeType: isYoutubeType, + parseSpeed: parseSpeed, + parseYoutubeStreams: parseYoutubeStreams, + setPlayerMode: setPlayerMode, + setSpeed: setSpeed, + setAutoAdvance: setAutoAdvance, + speedToString: speedToString, + trigger: trigger, + youtubeId: youtubeId, + loadHtmlPlayer: loadHtmlPlayer, + loadYoutubePlayer: loadYoutubePlayer, + loadYouTubeIFrameAPI: loadYouTubeIFrameAPI +}; +/* eslint-enable no-use-before-define */ + +let _youtubeApiDeferred = null; +let _oldOnYouTubeIframeAPIReady; + +Initialize.prototype = methodsDict; + +export default Initialize; + +// *************************************************************** +// Private functions start here. Private functions start with underscore. +// *************************************************************** + +/** + * @function _makeFunctionsPublic + * + * Functions which will be accessible via 'state' object. When called, + * these functions will get the 'state' + * object as a context. + * + * @param {object} state The object containg the state (properties, + * methods, modules) of the Video player. + */ +function _makeFunctionsPublic(state) { + bindTo(methodsDict, state, state); +} + +// function _renderElements(state) +// +// Create any necessary DOM elements, attach them, and set their +// initial configuration. Also make the created DOM elements available +// via the 'state' object. Much easier to work this way - you don't +// have to do repeated jQuery element selects. +function _renderElements(state) { + // Launch embedding of actual video content, or set it up so that it + // will be done as soon as the appropriate video player (YouTube or + // stand-alone HTML5) is loaded, and can handle embedding. + // + // Note that the loading of stand alone HTML5 player API is handled by + // Require JS. At the time when we reach this code, the stand alone + // HTML5 player is already loaded, so no further testing in that case + // is required. + let video; + let onYTApiReady; + let setupOnYouTubeIframeAPIReady; + + if (state.videoType === 'youtube') { + state.youtubeApiAvailable = false; + + onYTApiReady = function() { + console.log('[Video info]: YouTube API is available and is loaded.'); + if (state.htmlPlayerLoaded) { return; } + + console.log('[Video info]: Starting YouTube player.'); + video = VideoPlayer(state); + + state.modules.push(video); + state.__dfd__.resolve(); + state.youtubeApiAvailable = true; + }; + + if (window.YT) { + // If we have a Deferred object responsible for calling OnYouTubeIframeAPIReady + // callbacks, make sure that they have all been called by trying to resolve the + // Deferred object. Upon resolving, all the OnYouTubeIframeAPIReady will be + // called. If the object has been already resolved, the callbacks will not + // be called a second time. + if (_youtubeApiDeferred) { + _youtubeApiDeferred.resolve(); + } + + window.YT.ready(onYTApiReady); + } else { + // There is only one global variable window.onYouTubeIframeAPIReady which + // is supposed to be a function that will be called by the YouTube API + // when it finished initializing. This function will update this global function + // so that it resolves our Deferred object, which will call all of the + // OnYouTubeIframeAPIReady callbacks. + // + // If this global function is already defined, we store it first, and make + // sure that it gets executed when our Deferred object is resolved. + setupOnYouTubeIframeAPIReady = function() { + _oldOnYouTubeIframeAPIReady = window.onYouTubeIframeAPIReady || undefined; + + window.onYouTubeIframeAPIReady = function() { + _youtubeApiDeferred.resolve(); + }; + + window.onYouTubeIframeAPIReady.done = _youtubeApiDeferred.done; + + if (_oldOnYouTubeIframeAPIReady) { + window.onYouTubeIframeAPIReady.done(_oldOnYouTubeIframeAPIReady); + } + }; + + // If a Deferred object hasn't been created yet, create one now. It will + // be responsible for calling OnYouTubeIframeAPIReady callbacks once the + // YouTube API loads. After creating the Deferred object, load the YouTube + // API. + if (!_youtubeApiDeferred) { + _youtubeApiDeferred = $.Deferred(); + setupOnYouTubeIframeAPIReady(); + } else if (!window.onYouTubeIframeAPIReady || !window.onYouTubeIframeAPIReady.done) { + // The Deferred object could have been already defined in a previous + // initialization of the video module. However, since then the global variable + // window.onYouTubeIframeAPIReady could have been overwritten. If so, + // we should set it up again. + setupOnYouTubeIframeAPIReady(); + } + + // Attach a callback to our Deferred object to be called once the + // YouTube API loads. + window.onYouTubeIframeAPIReady.done(function() { + window.YT.ready(onYTApiReady); + }); + } + } else { + video = VideoPlayer(state); + + state.modules.push(video); + state.__dfd__.resolve(); + state.htmlPlayerLoaded = true; + } +} + +function _waitForYoutubeApi(state) { + console.log('[Video info]: Starting to wait for YouTube API to load.'); + window.setTimeout(function() { + // If YouTube API will load OK, it will run `onYouTubeIframeAPIReady` + // callback, which will set `state.youtubeApiAvailable` to `true`. + // If something goes wrong at this stage, `state.youtubeApiAvailable` is + // `false`. + if (!state.youtubeApiAvailable) { + console.log('[Video info]: YouTube API is not available.'); + if (!state.htmlPlayerLoaded) { + state.loadHtmlPlayer(); + } + } + state.el.trigger('youtube_availability', [state.youtubeApiAvailable]); + }, state.config.ytTestTimeout); +} + +function loadYouTubeIFrameAPI(scriptTag) { + let firstScriptTag = document.getElementsByTagName('script')[0]; + firstScriptTag.parentNode.insertBefore(scriptTag, firstScriptTag); +} + +// function _parseYouTubeIDs(state) +// The function parse YouTube stream ID's. +// @return +// false: We don't have YouTube video IDs to work with; most likely +// we have HTML5 video sources. +// true: Parsing of YouTube video IDs went OK, and we can proceed +// onwards to play YouTube videos. +function _parseYouTubeIDs(state) { + if (state.parseYoutubeStreams(state.config.streams)) { + state.videoType = 'youtube'; + + return true; + } + + console.log( + '[Video info]: Youtube Video IDs are incorrect or absent.' + ); + + return false; +} + +/** + * Extract HLS video URLs from available video URLs. + * + * @param {object} state The object contaning the state (properties, methods, modules) of the Video player. + * @returns Array of available HLS video source urls. + */ +function extractHLSVideoSources(state) { + return _.filter(state.config.sources, function(source) { + return /\.m3u8(\?.*)?$/.test(source); + }); +} + +// function _prepareHTML5Video(state) +// The function prepare HTML5 video, parse HTML5 +// video sources etc. +function _prepareHTML5Video(state) { + state.speeds = ['0.75', '1.0', '1.25', '1.50', '2.0']; + // If none of the supported video formats can be played and there is no + // short-hand video links, than hide the spinner and show error message. + if (!state.config.sources.length) { + _hideWaitPlaceholder(state); + state.el + .find('.video-player div') + .addClass('hidden'); + state.el + .find('.video-player .video-error') + .removeClass('is-hidden'); + + return false; + } + + state.videoType = 'html5'; + + if (!_.keys(state.config.transcriptLanguages).length) { + state.config.showCaptions = false; + } + state.setSpeed(state.speed); + + return true; +} + +function _hideWaitPlaceholder(state) { + state.el + .addClass('is-initialized') + .find('.spinner') + .attr({ + 'aria-hidden': 'true', + tabindex: -1 + }); +} + +function _setConfigurations(state) { + state.setPlayerMode(state.config.mode); + // Possible value are: 'visible', 'hiding', and 'invisible'. + state.controlState = 'visible'; + state.controlHideTimeout = null; + state.captionState = 'invisible'; + state.captionHideTimeout = null; + state.HLSVideoSources = extractHLSVideoSources(state); +} + +// eslint-disable-next-line no-shadow +function _initializeModules(state, i18n) { + let dfd = $.Deferred(), + modulesList = $.map(state.modules, function(module) { + let options = state.options[module.moduleName] || {}; + if (_.isFunction(module)) { + return module(state, i18n, options); + } else if ($.isPlainObject(module)) { + return module; + } + }); + + $.when.apply(null, modulesList) + .done(dfd.resolve); + + return dfd.promise(); +} + +function _getConfiguration(data, storage) { + let isBoolean = function(value) { + let regExp = /^true$/i; + return regExp.test(value.toString()); + }, + // List of keys that will be extracted form the configuration. + extractKeys = [], + // Compatibility keys used to change names of some parameters in + // the final configuration. + compatKeys = { + start: 'startTime', + end: 'endTime' + }, + // Conversions used to pre-process some configuration data. + conversions = { + showCaptions: isBoolean, + autoplay: isBoolean, + autohideHtml5: isBoolean, + autoAdvance: function(value) { + let shouldAutoAdvance = storage.getItem('auto_advance'); + if (_.isUndefined(shouldAutoAdvance)) { + return isBoolean(value) || false; + } else { + return shouldAutoAdvance; + } + }, + savedVideoPosition: function(value) { + return storage.getItem('savedVideoPosition', true) + || Number(value) + || 0; + }, + speed: function(value) { + return storage.getItem('speed', true) || value; + }, + generalSpeed: function(value) { + return storage.getItem('general_speed') + || value + || '1.0'; + }, + transcriptLanguage: function(value) { + return storage.getItem('language') + || value + || 'en'; + }, + ytTestTimeout: function(value) { + value = parseInt(value, 10); + + if (!isFinite(value)) { + value = 1500; + } + + return value; + }, + startTime: function(value) { + value = parseInt(value, 10); + if (!isFinite(value) || value < 0) { + return 0; + } + + return value; + }, + endTime: function(value) { + value = parseInt(value, 10); + + if (!isFinite(value) || value === 0) { + return null; + } + + return value; + } + }, + config = {}; + + data = _.extend({ + startTime: 0, + endTime: null, + sub: '', + streams: '' + }, data); + + $.each(data, function(option, value) { + // Extract option that is in `extractKeys`. + if ($.inArray(option, extractKeys) !== -1) { + return; + } + + // Change option name to key that is in `compatKeys`. + if (compatKeys[option]) { + option = compatKeys[option]; + } + + // Pre-process data. + if (conversions[option]) { + if (_.isFunction(conversions[option])) { + value = conversions[option].call(this, value); + } else { + throw new TypeError(option + ' is not a function.'); + } + } + config[option] = value; + }); + + return config; +} + +// *************************************************************** +// Public functions start here. +// These are available via the 'state' object. Their context ('this' +// keyword) is the 'state' object. The magic private function that makes +// them available and sets up their context is makeFunctionsPublic(). +// *************************************************************** + +// function bindTo(methodsDict, obj, context, rewrite) +// Creates a new function with specific context and assigns it to the provided +// object. +// eslint-disable-next-line no-shadow +function bindTo(methodsDict, obj, context, rewrite) { + $.each(methodsDict, function(name, method) { + if (_.isFunction(method)) { + if (_.isUndefined(rewrite)) { + rewrite = true; + } + + if (_.isUndefined(obj[name]) || rewrite) { + obj[name] = _.bind(method, context); + } + } + }); +} + +function loadYoutubePlayer() { + if (this.htmlPlayerLoaded) { return; } + + console.log( + '[Video info]: Fetch metadata for YouTube video.' + ); + + this.fetchMetadata(); + this.parseSpeed(); +} + +function loadHtmlPlayer() { + // When the youtube link doesn't work for any reason + // (for example, firewall) any + // alternate sources should automatically play. + if (!_prepareHTML5Video(this)) { + console.log( + '[Video info]: Continue loading ' + + 'YouTube video.' + ); + + // Non-YouTube sources were not found either. + + this.el.find('.video-player div') + .removeClass('hidden'); + this.el.find('.video-player .video-error') + .addClass('is-hidden'); + + // If in reality the timeout was to short, try to + // continue loading the YouTube video anyways. + this.loadYoutubePlayer(); + } else { + console.log( + '[Video info]: Start HTML5 player.' + ); + + // In-browser HTML5 player does not support quality + // control. + this.el.find('.quality_control').hide(); + _renderElements(this); + } +} + +// function initialize(element) +// The function set initial configuration and preparation. + +function initialize(element) { + let self = this, + el = this.el, + id = this.id, + container = el.find('.video-wrapper'), + __dfd__ = $.Deferred(), + isTouch = onTouchBasedDevice() || ''; + + if (isTouch) { + el.addClass('is-touch'); + } + + $.extend(this, { + __dfd__: __dfd__, + container: container, + isFullScreen: false, + isTouch: isTouch + }); + + console.log('[Video info]: Initializing video with id "%s".', id); + + // We store all settings passed to us by the server in one place. These + // are "read only", so don't modify them. All variable content lives in + // 'state' object. + // jQuery .data() return object with keys in lower camelCase format. + this.config = $.extend({}, _getConfiguration(this.metadata, this.storage), { + element: element, + fadeOutTimeout: 1400, + captionsFreezeTime: 10000, + mode: $.cookie('edX_video_player_mode'), + // Available HD qualities will only be accessible once the video has + // been played once, via player.getAvailableQualityLevels. + availableHDQualities: [] + }); + + if (this.config.endTime < this.config.startTime) { + this.config.endTime = null; + } + + this.lang = this.config.transcriptLanguage; + this.speed = this.speedToString( + this.config.speed || this.config.generalSpeed + ); + this.auto_advance = this.config.autoAdvance; + this.htmlPlayerLoaded = false; + this.duration = this.metadata.duration; + + _setConfigurations(this); + + // If `prioritizeHls` is set to true than `hls` is the primary playback + if (this.config.prioritizeHls || !(_parseYouTubeIDs(this))) { + // If we do not have YouTube ID's, try parsing HTML5 video sources. + if (!_prepareHTML5Video(this)) { + __dfd__.reject(); + // Non-YouTube sources were not found either. + return __dfd__.promise(); + } + + console.log('[Video info]: Start player in HTML5 mode.'); + _renderElements(this); + } else { + _renderElements(this); + + _waitForYoutubeApi(this); + + let scriptTag = document.createElement('script'); + + scriptTag.src = this.config.ytApiUrl; + scriptTag.async = true; + + $(scriptTag).on('load', function() { + self.loadYoutubePlayer(); + }); + $(scriptTag).on('error', function() { + console.log( + '[Video info]: YouTube returned an error for ' + + 'video with id "' + self.id + '".' + ); + // If the video is already loaded in `_waitForYoutubeApi` by the + // time we get here, then we shouldn't load it again. + if (!self.htmlPlayerLoaded) { + self.loadHtmlPlayer(); + } + }); + + window.Video.loadYouTubeIFrameAPI(scriptTag); + } + return __dfd__.promise(); +} + +// function parseYoutubeStreams(state, youtubeStreams) +// +// Take a string in the form: +// "iCawTYPtehk:0.75,KgpclqP-LBA:1.0,9-2670d5nvU:1.5" +// parse it, and make it available via the 'state' object. If we are +// not given a string, or it's length is zero, then we return false. +// +// @return +// false: We don't have YouTube video IDs to work with; most likely +// we have HTML5 video sources. +// true: Parsing of YouTube video IDs went OK, and we can proceed +// onwards to play YouTube videos. +function parseYoutubeStreams(youtubeStreams) { + if (_.isUndefined(youtubeStreams) || !youtubeStreams.length) { + return false; + } + + this.videos = {}; + + _.each(youtubeStreams.split(/,/), function(video) { + let speed; + video = video.split(/:/); + speed = this.speedToString(video[0]); + this.videos[speed] = video[1]; + }, this); + + return _.isString(this.videos['1.0']); +} + +// function fetchMetadata() +// +// When dealing with YouTube videos, we must fetch meta data that has +// certain key facts not available while the video is loading. For +// example the length of the video can be determined from the meta +// data. +function fetchMetadata() { + let self = this, + metadataXHRs = []; + + this.metadata = {}; + + metadataXHRs = _.map(this.videos, function(url, speed) { + return self.getVideoMetadata(url, function(data) { + if (data.items.length > 0) { + let metaDataItem = data.items[0]; + self.metadata[metaDataItem.id] = metaDataItem.contentDetails; + } + }); + }); + + $.when.apply(this, metadataXHRs).done(function() { + self.el.trigger('metadata_received'); + + // Not only do we trigger the "metadata_received" event, we also + // set a flag to notify that metadata has been received. This + // allows for code that will miss the "metadata_received" event + // to know that metadata has been received. This is important in + // cases when some code will subscribe to the "metadata_received" + // event after it has been triggered. + self.youtubeMetadataReceived = true; + }); +} + +// function parseSpeed() +// +// Create a separate array of available speeds. +function parseSpeed() { + this.speeds = _.keys(this.videos).sort(); +} + +function setSpeed(newSpeed) { + // Possible speeds for each player type. + // HTML5 = [0.75, 1, 1.25, 1.5, 2] + // Youtube Flash = [0.75, 1, 1.25, 1.5] + // Youtube HTML5 = [0.25, 0.5, 1, 1.5, 2] + let map = { + 0.25: '0.75', // Youtube HTML5 -> HTML5 or Youtube Flash + '0.50': '0.75', // Youtube HTML5 -> HTML5 or Youtube Flash + 0.75: '0.50', // HTML5 or Youtube Flash -> Youtube HTML5 + 1.25: '1.50', // HTML5 or Youtube Flash -> Youtube HTML5 + 2.0: '1.50' // HTML5 or Youtube HTML5 -> Youtube Flash + }; + + if (_.contains(this.speeds, newSpeed)) { + this.speed = newSpeed; + } else { + newSpeed = map[newSpeed]; + this.speed = _.contains(this.speeds, newSpeed) ? newSpeed : '1.0'; + } + this.speed = parseFloat(this.speed); +} + +function setAutoAdvance(enabled) { + this.auto_advance = enabled; +} + +function getVideoMetadata(url, callback) { + let youTubeEndpoint; + if (!(_.isString(url))) { + url = this.videos['1.0'] || ''; + } + // Will hit the API URL to get the youtube video metadata. + youTubeEndpoint = this.config.ytMetadataEndpoint; // The new runtime supports anonymous users + // and uses an XBlock handler to get YouTube metadata + if (!youTubeEndpoint) { + // The old runtime has a full/separate LMS API for getting YouTube metadata, but it doesn't + // support anonymous users nor videos that play in a sandboxed iframe. + youTubeEndpoint = [this.config.lmsRootURL, '/courses/yt_video_metadata', '?id=', url].join(''); + } + return $.ajax({ + url: youTubeEndpoint, + success: _.isFunction(callback) ? callback : null, + error: function() { + console.warn( + 'Unable to get youtube video metadata. Some video metadata may be unavailable.' + ); + }, + notifyOnError: false + }); +} + +function youtubeId(speed) { + let currentSpeed = this.isFlashMode() ? this.speed : '1.0'; + + return this.videos[speed] + || this.videos[currentSpeed] + || this.videos['1.0']; +} + +function getDuration() { + try { + let safeMoment = typeof moment !== 'undefined' ? moment : window.moment; + return safeMoment.duration(this.metadata[this.youtubeId()].duration, safeMoment.ISO_8601).asSeconds(); + } catch (err) { + return _.result(this.metadata[this.youtubeId('1.0')], 'duration') || 0; + } +} + +/** + * Sets player mode. + * + * @param {string} mode Mode to set for the video player if it is supported. + * Otherwise, `html5` is used by default. + */ +function setPlayerMode(mode) { + let supportedModes = ['html5', 'flash']; + + mode = _.contains(supportedModes, mode) ? mode : 'html5'; + this.currentPlayerMode = mode; +} + +/** + * Returns current player mode. + * + * @return {string} Returns string that describes player mode + */ +function getPlayerMode() { + return this.currentPlayerMode; +} + +/** + * Checks if current player mode is Flash. + * + * @return {boolean} Returns `true` if current mode is `flash`, otherwise + * it returns `false` + */ +function isFlashMode() { + return this.getPlayerMode() === 'flash'; +} + +/** + * Checks if current player mode is Html5. + * + * @return {boolean} Returns `true` if current mode is `html5`, otherwise + * it returns `false` + */ +function isHtml5Mode() { + return this.getPlayerMode() === 'html5'; +} + +function isYoutubeType() { + return this.videoType === 'youtube'; +} + +function speedToString(speed) { + return parseFloat(speed).toFixed(2).replace(/\.00$/, '.0'); +} + +function getCurrentLanguage() { + let keys = _.keys(this.config.transcriptLanguages); + + if (keys.length) { + if (!_.contains(keys, this.lang)) { + if (_.contains(keys, 'en')) { + this.lang = 'en'; + } else { + this.lang = keys.pop(); + } + } + } else { + return null; + } + + return this.lang; +} + +/* + * The trigger() function will assume that the @objChain is a complete + * chain with a method (function) at the end. It will call this function. + * So for example, when trigger() is called like so: + * + * state.trigger('videoPlayer.pause', {'param1': 10}); + * + * Then trigger() will execute: + * + * state.videoPlayer.pause({'param1': 10}); + */ +function trigger(objChain) { + let extraParameters = Array.prototype.slice.call(arguments, 1), + i, tmpObj, chain; + + // Remember that 'this' is the 'state' object. + tmpObj = this; + chain = objChain.split('.'); + + // At the end of the loop the variable 'tmpObj' will either be the + // correct object/function to trigger/invoke. If the 'chain' chain of + // object is incorrect (one of the link is non-existent), then the loop + // will immediately exit. + while (chain.length) { + i = chain.shift(); + + if (tmpObj.hasOwnProperty(i)) { + tmpObj = tmpObj[i]; + } else { + // An incorrect object chain was specified. + + return false; + } + } + + tmpObj.apply(this, extraParameters); + + return true; +} diff --git a/xblocks_contrib/video/assets/js/src/025_focus_grabber.js b/xblocks_contrib/video/assets/js/src/025_focus_grabber.js new file mode 100644 index 00000000..48ec5527 --- /dev/null +++ b/xblocks_contrib/video/assets/js/src/025_focus_grabber.js @@ -0,0 +1,132 @@ +/* + * 025_focus_grabber.js + * + * Purpose: Provide a way to focus on autohidden Video controls. + * + * + * Because in HTML player mode we have a feature of autohiding controls on + * mouse inactivity, sometimes focus is lost from the currently selected + * control. What's more, when all controls are autohidden, we can't get to any + * of them because by default browser does not place hidden elements on the + * focus chain. + * + * To get around this minor annoyance, this module will manage 2 placeholder + * elements that will be invisible to the user's eye, but visible to the + * browser. This will allow for a sneaky stealing of focus and placing it where + * we need (on hidden controls). + * + * This code has been moved to a separate module because it provides a concrete + * block of functionality that can be turned on (off). + */ + +/* + * "If you want to climb a mountain, begin at the top." + * + * ~ Zen saying + */ + + + +// FocusGrabber module. +let FocusGrabber = function(state) { + let dfd = $.Deferred(); + + state.focusGrabber = {}; + + _makeFunctionsPublic(state); + _renderElements(state); + _bindHandlers(state); + + dfd.resolve(); + return dfd.promise(); +}; + +// Private functions. + +function _makeFunctionsPublic(state) { + let methodsDict = { + disableFocusGrabber: disableFocusGrabber, + enableFocusGrabber: enableFocusGrabber, + onFocus: onFocus + }; + + state.bindTo(methodsDict, state.focusGrabber, state); +} + +function _renderElements(state) { + state.focusGrabber.elFirst = state.el.find('.focus_grabber.first'); + state.focusGrabber.elLast = state.el.find('.focus_grabber.last'); + + // From the start, the Focus Grabber must be disabled so that + // tabbing (switching focus) does not land the user on one of the + // placeholder elements (elFirst, elLast). + state.focusGrabber.disableFocusGrabber(); +} + +function _bindHandlers(state) { + state.focusGrabber.elFirst.on('focus', state.focusGrabber.onFocus); + state.focusGrabber.elLast.on('focus', state.focusGrabber.onFocus); + + // When the video container element receives programmatic focus, then + // on un-focus ('blur' event) we should trigger a 'mousemove' event so + // as to reveal autohidden controls. + state.el.on('blur', function() { + state.el.trigger('mousemove'); + }); +} + +// Public functions. + +function enableFocusGrabber() { + let tabIndex; + + // When the Focus Grabber is being enabled, there are two different + // scenarios: + // + // 1.) Currently focused element was inside the video player. + // 2.) Currently focused element was somewhere else on the page. + // + // In the first case we must make sure that the video player doesn't + // loose focus, even though the controls are autohidden. + if ($(document.activeElement).parents().hasClass('video')) { + tabIndex = -1; + } else { + tabIndex = 0; + } + + this.focusGrabber.elFirst.attr('tabindex', tabIndex); + this.focusGrabber.elLast.attr('tabindex', tabIndex); + + // Don't loose focus. We are inside video player on some control, but + // because we can't remain focused on a hidden element, we will shift + // focus to the main video element. + // + // Once the main element will receive the un-focus ('blur') event, a + // 'mousemove' event will be triggered, and the video controls will + // receive focus once again. + if (tabIndex === -1) { + this.el.focus(); + + this.focusGrabber.elFirst.attr('tabindex', 0); + this.focusGrabber.elLast.attr('tabindex', 0); + } +} + +function disableFocusGrabber() { + // Only programmatic focusing on these elements will be available. + // We don't want the user to focus on them (for example with the 'Tab' + // key). + this.focusGrabber.elFirst.attr('tabindex', -1); + this.focusGrabber.elLast.attr('tabindex', -1); +} + +function onFocus(event, params) { + // Once the Focus Grabber placeholder elements will gain focus, we will + // trigger 'mousemove' event so that the autohidden controls will + // become visible. + this.el.trigger('mousemove'); + + this.focusGrabber.disableFocusGrabber(); +} + +export default FocusGrabber; diff --git a/xblocks_contrib/video/assets/js/src/02_html5_hls_video.js b/xblocks_contrib/video/assets/js/src/02_html5_hls_video.js new file mode 100644 index 00000000..7a876c1a --- /dev/null +++ b/xblocks_contrib/video/assets/js/src/02_html5_hls_video.js @@ -0,0 +1,151 @@ +/* eslint-disable no-console, no-param-reassign */ +/** + * HTML5 video player module to support HLS video playback. + * + */ + +'use strict'; + +import _ from 'underscore'; +import HTML5Video from './02_html5_video.js'; +import Hls from 'hls.js'; + +let HLSVideo = {}; + +HLSVideo.Player = (function() { + /** + * Initialize HLS video player. + * + * @param {jQuery} el Reference to video player container element + * @param {Object} config Contains common config for video player + */ + function Player(el, config) { + let self = this; + + this.config = config; + + // do common initialization independent of player type + this.init(el, config); + + // set a default audio codec if not provided, this helps reduce issues + // switching audio codecs during playback + if (!this.config.defaultAudioCodec) { + this.config.defaultAudioCodec = "mp4a.40.5"; + } + + _.bindAll(this, 'playVideo', 'pauseVideo', 'onReady'); + + // If we have only HLS sources and browser doesn't support HLS then show error message. + if (config.HLSOnlySources && !config.canPlayHLS) { + this.showErrorMessage(null, '.video-hls-error'); + return; + } + + this.config.state.el.on('initialize', _.once(function() { + console.log('[HLS Video]: HLS Player initialized'); + self.showPlayButton(); + })); + + // Safari has native support to play HLS videos + if (config.browserIsSafari) { + this.videoEl.attr('src', config.videoSources[0]); + } else { + // load auto start if auto_advance is enabled + if (config.state.auto_advance) { + this.hls = new Hls({autoStartLoad: true}); + } else { + this.hls = new Hls({autoStartLoad: false}); + } + this.hls.loadSource(config.videoSources[0]); + this.hls.attachMedia(this.video); + + this.hls.on(Hls.Events.ERROR, this.onError.bind(this)); + + this.hls.on(Hls.Events.MANIFEST_PARSED, function(event, data) { + console.log( + '[HLS Video]: MANIFEST_PARSED, qualityLevelsInfo: ', + data.levels.map(function(level) { + return { + bitrate: level.bitrate, + resolution: level.width + 'x' + level.height + }; + }) + ); + self.config.onReadyHLS(); + }); + this.hls.on(Hls.Events.LEVEL_SWITCHED, function(event, data) { + let level = self.hls.levels[data.level]; + console.log( + '[HLS Video]: LEVEL_SWITCHED, qualityLevelInfo: ', + { + bitrate: level.bitrate, + resolution: level.width + 'x' + level.height + } + ); + }); + } + } + + Player.prototype = Object.create(HTML5Video.Player.prototype); + Player.prototype.constructor = Player; + + Player.prototype.playVideo = function() { + HTML5Video.Player.prototype.updatePlayerLoadingState.apply(this, ['show']); + if (!this.config.browserIsSafari) { + this.hls.startLoad(); + } + HTML5Video.Player.prototype.playVideo.apply(this); + }; + + Player.prototype.pauseVideo = function() { + HTML5Video.Player.prototype.pauseVideo.apply(this); + HTML5Video.Player.prototype.updatePlayerLoadingState.apply(this, ['hide']); + }; + + Player.prototype.onPlaying = function() { + HTML5Video.Player.prototype.onPlaying.apply(this); + HTML5Video.Player.prototype.updatePlayerLoadingState.apply(this, ['hide']); + }; + + Player.prototype.onReady = function() { + this.config.events.onReady(null); + }; + + /** + * Handler for HLS video errors. This only takes care of fatal erros, non-fatal errors + * are automatically handled by hls.js + * + * @param {String} event `hlsError` + * @param {Object} data Contains the information regarding error occurred. + */ + Player.prototype.onError = function(event, data) { + if (data.fatal) { + switch (data.type) { + case Hls.ErrorTypes.NETWORK_ERROR: + console.error( + '[HLS Video]: Fatal network error encountered, try to recover. Details: %s', + data.details + ); + this.hls.startLoad(); + break; + case Hls.ErrorTypes.MEDIA_ERROR: + console.error( + '[HLS Video]: Fatal media error encountered, try to recover. Details: %s', + data.details + ); + this.hls.recoverMediaError(); + break; + default: + console.error( + '[HLS Video]: Unrecoverable error encountered. Details: %s', + data.details + ); + break; + } + } + }; + + return Player; +}()); + +export default HLSVideo; diff --git a/xblocks_contrib/video/assets/js/src/02_html5_video.js b/xblocks_contrib/video/assets/js/src/02_html5_video.js new file mode 100644 index 00000000..83937205 --- /dev/null +++ b/xblocks_contrib/video/assets/js/src/02_html5_video.js @@ -0,0 +1,380 @@ +/* eslint-disable no-console, no-param-reassign */ +/** + * @file HTML5 video player module. Provides methods to control the in-browser + * HTML5 video player. + * + * The goal was to write this module so that it closely resembles the YouTube + * API. The main reason for this is because initially the edX video player + * supported only YouTube videos. When HTML5 support was added, for greater + * compatibility, and to reduce the amount of code that needed to be modified, + * it was decided to write a similar API as the one provided by YouTube. + * + * @module HTML5Video + */ + +import _ from 'underscore'; + +let HTML5Video = {}; + +HTML5Video.Player = (function() { + /* + * Constructor function for HTML5 Video player. + * + * @param {String|Object} el A DOM element where the HTML5 player will + * be inserted (as returned by jQuery(selector) function), or a + * selector string which will be used to select an element. This is a + * required parameter. + * + * @param config - An object whose properties will be used as + * configuration options for the HTML5 video player. This is an + * optional parameter. In the case if this parameter is missing, or + * some of the config object's properties are missing, defaults will be + * used. The available options (and their defaults) are as + * follows: + * + * config = { + * + * videoSources: [], // An array with properties being video + * // sources. The property name is the + * // video format of the source. Supported + * // video formats are: 'mp4', 'webm', and + * // 'ogg'. + * poster: Video poster URL + * + * browserIsSafari: Flag to tell if current browser is Safari + * + * events: { // Object's properties identify the + * // events that the API fires, and the + * // functions (event listeners) that the + * // API will call when those events occur. + * // If value is null, or property is not + * // specified, then no callback will be + * // called for that event. + * + * onReady: null, + * onStateChange: null + * } + * } + */ + function Player(el, config) { + let errorMessage, lastSource, sourceList; + + // Create HTML markup for individual sources of the HTML5