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..18659004 100644 --- a/package-lock.json +++ b/package-lock.json @@ -287,7 +287,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 +609,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 +619,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 +628,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 +646,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 +670,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 +679,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 +708,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 +727,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 +736,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 +744,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 +767,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 +780,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 +792,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 +806,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 +862,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 +889,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 +901,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 +1074,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 +1462,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 +1642,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 +1689,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 +1722,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 +1789,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 +2028,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 +2097,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 +2286,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 +2345,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 +2380,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 +2393,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 +2405,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 +2414,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 +2435,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 +2772,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 +2949,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 +3050,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 +3257,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", @@ -3608,6 +3693,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 +3950,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 +3964,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" }, @@ -3887,8 +4003,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", @@ -4053,7 +4168,6 @@ "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 +4381,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 +4675,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 +4894,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", @@ -4909,6 +5030,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 +5072,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", @@ -5838,7 +5995,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 +6007,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 +6378,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 +6422,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 +6432,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 +6574,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 +6592,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 +6812,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 +7019,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 +7038,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 +7051,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 +7150,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 +7208,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 +7253,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": { @@ -7147,6 +7349,36 @@ "string-replace-loader": "^3", "webpack-cli": "^5" } + }, + "xblocks_contrib/video/assets": { + "name": "video-xblock", + "dependencies": { + "draggabilly": "^3", + "edx-ui-toolkit": "^1", + "hls.js": "^1", + "underscore": "^1", + "webpack-merge": "^6" + }, + "devDependencies": { + "clean-webpack-plugin": "^4", + "copy-webpack-plugin": "^13", + "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..d1ee6996 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 @@ -544,6 +596,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/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