mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-27 01:21:20 -08:00
refactor(core): CORE-15 type subtitles and own request and track lifetimes
This commit is contained in:
1 parent
64d1939daa
commit
1b83081e8f
28 files changed
+11286
-405
No files matched your search
@@ -4,6 +4,38 @@ ArtPlayer keeps its existing constructor, player mixins, plugins and DOM/CSS hoo
|
||||
The production entry is still `src/index.js`; this document marks actual migrated
|
||||
boundaries rather than describing the entire core as TypeScript.
|
||||
|
||||
## Subtitles (CORE-15)
|
||||
|
||||
src/subtitle/index.ts keeps the Component prototype, bound update, public methods and
|
||||
normal URL results. The request, parse, state, track and render modules now separate
|
||||
transport cancellation, conversion, ownership, native DOM events and cue rendering.
|
||||
The offset mixin remains the player property entry, with original cue bounds retained
|
||||
on native cue objects. Validation evidence and remaining release gates live in refactor/.
|
||||
|
||||
request.ts races work against scope closure. Superseded requests settle undefined even
|
||||
when the transport ignores AbortSignal; active failures reject. state.ts owns request
|
||||
and track scopes and only generated object URLs. A caller-owned URL is never revoked.
|
||||
Replacing a track for native fullscreen keeps the same owned subtitle resource.
|
||||
parse.ts delegates SRT/ASS conversion to the public utility parsers, preserving callback
|
||||
this and type/extension/encoding behavior. Unknown formats retain the caller URL.
|
||||
render.ts retains the line/group markup and uses current global subtitle.escape; a
|
||||
switch-specific escape override does not silently change the legacy rendering contract.
|
||||
Before-update listeners may still edit cues; switching/destroying prevents obsolete DOM
|
||||
writes. Disabled text tracks return empty cue arrays; native cue identity is preserved.
|
||||
|
||||
To change transport policy start with request.ts and index.ts; to change native track
|
||||
ownership start with track.ts and state.ts. Run test/subtitle.test.js, the browser
|
||||
subtitle-lifecycle and declarations specs, and strict source/consumer type checks.
|
||||
Track generations prevent a reentrant replacement from losing its callbacks when an
|
||||
older registration returns or throws. Failed insertion restores the previous node;
|
||||
cleanup failures after commit retain the new resource and remain observable to callers.
|
||||
Track load errors notify only while that node is current; all owned load/error/cuechange
|
||||
callbacks are cleared on replacement or destroy. HTTP errors reject before conversion.
|
||||
Constructor and URL-setter calls report through notice and consume their otherwise
|
||||
unobservable rejected promises; explicit init/switch calls keep rejection semantics.
|
||||
Public cue event inference remains tracked by BASE-TYPE-07 / CORE-21. Browser-engine
|
||||
tests do not certify physical iOS/Safari fullscreen or every proxy implementation.
|
||||
|
||||
## Settings (CORE-14)
|
||||
|
||||
setting/index.ts remains the specialized manager: panel cache keys are option arrays,
|
||||
|
||||
Vendored
+1
-1
@@ -67,4 +67,4 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";function t(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var e,r={exports:{}};var o=(e||(e=1,r.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var n=t(r);if("boolean"===n)return"boolean";if("string"===n)return"string";if("number"===n)return"number";if("symbol"===n)return"symbol";if("function"===n)return"GeneratorFunction"===o(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(o(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(n=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return n.slice(8,-1).toLowerCase().replace(/\s/g,"")};function o(t){return t.constructor?t.constructor.name:null}function n(t,e){var o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return a(t,e,o),i(t,e,o),function(t,e,o){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=o.slice();c.push(r),a(s,l,c),i(s,l,c),n(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=o.slice();d.push(s),a(l,c,d),i(l,c,d),n(l,c,d)})}}(t,e,o),t}function a(t,e,o){if("string"===r(e)){var n=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return n===t}):e.toLowerCase().trim()===n))throw new Error("[Type Error]: '".concat(o.join("."),"' require '").concat(e,"' type, but got '").concat(n,"'"))}}function i(t,e,o){if("function"===r(e)){var n=e(t,r(t),o);if(!0!==n){var a=r(n);throw"string"===a?new Error(n):"error"===a?n:new Error("[Validator Error]: The scheme for '".concat(o.join("."),"' validator require return true, but got '").concat(n,"'"))}}}return n.kindOf=r,n}()),r.exports);const n=t(o),a="5.4.1",i={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","mediaGroup","muted","networkState","paused","playbackRate","played","preload","readyState","seekable","seeking","src","startDate","textTracks","videoTracks","volume"],methods:["addTextTrack","canPlayType","load","play","pause"],events:["abort","canplay","canplaythrough","durationchange","emptied","ended","error","loadeddata","loadedmetadata","loadstart","pause","play","playing","progress","ratechange","seeked","seeking","stalled","suspend","timeupdate","volumechange","waiting"],prototypes:["width","height","videoWidth","videoHeight","poster","webkitDecodedFrameCount","webkitDroppedFrameCount","playsInline","webLine truncated
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";function t(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var e,r={exports:{}};var o=(e||(e=1,r.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var n=t(r);if("boolean"===n)return"boolean";if("string"===n)return"string";if("number"===n)return"number";if("symbol"===n)return"symbol";if("function"===n)return"GeneratorFunction"===o(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(o(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(n=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return n.slice(8,-1).toLowerCase().replace(/\s/g,"")};function o(t){return t.constructor?t.constructor.name:null}function n(t,e){var o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return a(t,e,o),i(t,e,o),function(t,e,o){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=o.slice();c.push(r),a(s,l,c),i(s,l,c),n(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=o.slice();d.push(s),a(l,c,d),i(l,c,d),n(l,c,d)})}}(t,e,o),t}function a(t,e,o){if("string"===r(e)){var n=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return n===t}):e.toLowerCase().trim()===n))throw new Error("[Type Error]: '".concat(o.join("."),"' require '").concat(e,"' type, but got '").concat(n,"'"))}}function i(t,e,o){if("function"===r(e)){var n=e(t,r(t),o);if(!0!==n){var a=r(n);throw"string"===a?new Error(n):"error"===a?n:new Error("[Validator Error]: The scheme for '".concat(o.join("."),"' validator require return true, but got '").concat(n,"'"))}}}return n.kindOf=r,n}()),r.exports);const n=t(o),a="5.4.1",i={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","mediaGroup","muted","networkState","paused","playbackRate","played","preload","readyState","seekable","seeking","src","startDate","textTracks","videoTracks","volume"],methods:["addTextTrack","canPlayType","load","play","pause"],events:["abort","canplay","canplaythrough","durationchange","emptied","ended","error","loadeddata","loadedmetadata","loadstart","pause","play","playing","progress","ratechange","seeked","seeking","stalled","suspend","timeupdate","volumechange","waiting"],prototypes:["width","height","videoWidth","videoHeight","poster","webkitDecodedFrameCount","webkitDroppedFrameCount","playsInline","webLine truncated
|
||||
+1
-1
@@ -67,4 +67,4 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";var t,e=Object.defineProperty,r=Object.defineProperties,n=Object.getOwnPropertyDescriptors,o=Object.getOwnPropertySymbols,a=Object.prototype.hasOwnProperty,i=Object.prototype.propertyIsEnumerable,s=(t,r,n)=>r in t?e(t,r,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[r]=n,l=(t,e)=>{for(var r in e||(e={}))a.call(e,r)&&s(t,r,e[r]);if(o)for(var r of o(e))i.call(e,r)&&s(t,r,e[r]);return t},c=(t,e)=>r(t,n(e)),d=(t,e,r)=>new Promise((n,o)=>{var a=t=>{try{s(r.next(t))}catch(e){o(e)}},i=t=>{try{s(r.throw(t))}catch(e){o(e)}},s=t=>t.done?n(t.value):Promise.resolve(t.value).then(a,i);s((r=r.apply(t,e)).next())});function p(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var u,h={exports:{}};var f=(u||(u=1,h.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var o=t(r);if("boolean"===o)return"boolean";if("string"===o)return"string";if("number"===o)return"number";if("symbol"===o)return"symbol";if("function"===o)return"GeneratorFunction"===n(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(n(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(o=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return o.slice(8,-1).toLowerCase().replace(/\s/g,"")};function n(t){return t.constructor?t.constructor.name:null}function o(t,e){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return a(t,e,n),i(t,e,n),function(t,e,n){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=n.slice();c.push(r),a(s,l,c),i(s,l,c),o(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=n.slice();d.push(s),a(l,c,d),i(l,c,d),o(l,c,d)})}}(t,e,n),t}function a(t,e,n){if("string"===r(e)){var o=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return o===t}):e.toLowerCase().trim()===o))throw new Error("[Type Error]: '".concat(n.join("."),"' require '").concat(e,"' type, but got '").concat(o,"'"))}}function i(t,e,n){if("function"===r(e)){var o=e(t,r(t),n);if(!0!==o){var a=r(o);throw"string"===a?new Error(o):"error"===a?o:new Error("[Validator Error]: The scheme for '".concat(n.join("."),"' validator require return true, but got '").concat(o,"'"))}}}return o.kindOf=r,o}()),h.exports);const m=p(f),g="5.4.1",v={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","meLine truncated
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";var t,e=Object.defineProperty,r=Object.defineProperties,n=Object.getOwnPropertyDescriptors,o=Object.getOwnPropertySymbols,a=Object.prototype.hasOwnProperty,i=Object.prototype.propertyIsEnumerable,s=(t,r,n)=>r in t?e(t,r,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[r]=n,l=(t,e)=>{for(var r in e||(e={}))a.call(e,r)&&s(t,r,e[r]);if(o)for(var r of o(e))i.call(e,r)&&s(t,r,e[r]);return t},c=(t,e)=>r(t,n(e)),d=(t,e,r)=>new Promise((n,o)=>{var a=t=>{try{s(r.next(t))}catch(e){o(e)}},i=t=>{try{s(r.throw(t))}catch(e){o(e)}},s=t=>t.done?n(t.value):Promise.resolve(t.value).then(a,i);s((r=r.apply(t,e)).next())});function p(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var u,h={exports:{}};var f=(u||(u=1,h.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var o=t(r);if("boolean"===o)return"boolean";if("string"===o)return"string";if("number"===o)return"number";if("symbol"===o)return"symbol";if("function"===o)return"GeneratorFunction"===n(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(n(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(o=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return o.slice(8,-1).toLowerCase().replace(/\s/g,"")};function n(t){return t.constructor?t.constructor.name:null}function o(t,e){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return a(t,e,n),i(t,e,n),function(t,e,n){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=n.slice();c.push(r),a(s,l,c),i(s,l,c),o(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=n.slice();d.push(s),a(l,c,d),i(l,c,d),o(l,c,d)})}}(t,e,n),t}function a(t,e,n){if("string"===r(e)){var o=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return o===t}):e.toLowerCase().trim()===o))throw new Error("[Type Error]: '".concat(n.join("."),"' require '").concat(e,"' type, but got '").concat(o,"'"))}}function i(t,e,n){if("function"===r(e)){var o=e(t,r(t),n);if(!0!==o){var a=r(o);throw"string"===a?new Error(o):"error"===a?o:new Error("[Validator Error]: The scheme for '".concat(n.join("."),"' validator require return true, but got '").concat(o,"'"))}}}return o.kindOf=r,o}()),h.exports);const m=p(f),g="5.4.1",v={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","meLine truncated
|
||||
+259
-104
@@ -361,13 +361,13 @@ class ResourceScope {
|
||||
throw new ResourceCleanupError(errors);
|
||||
}
|
||||
}
|
||||
const states$1 = /* @__PURE__ */ new WeakMap();
|
||||
const states$2 = /* @__PURE__ */ new WeakMap();
|
||||
const containers = /* @__PURE__ */ new WeakMap();
|
||||
function beginLifecycle(owner) {
|
||||
states$1.set(owner, { scope: new ResourceScope(), destroying: false });
|
||||
states$2.set(owner, { scope: new ResourceScope(), destroying: false });
|
||||
}
|
||||
function stateOf(owner) {
|
||||
const state2 = states$1.get(owner);
|
||||
const state2 = states$2.get(owner);
|
||||
if (!state2)
|
||||
throw new Error("ArtPlayer lifecycle has not been initialized");
|
||||
return state2;
|
||||
@@ -1433,6 +1433,15 @@ function animationFrame(scope, callback) {
|
||||
});
|
||||
return release;
|
||||
}
|
||||
function requestController(scope) {
|
||||
if (typeof AbortController === "undefined")
|
||||
return void 0;
|
||||
const controller = new AbortController();
|
||||
scope.add(() => {
|
||||
controller.abort();
|
||||
});
|
||||
return controller;
|
||||
}
|
||||
function wait(scope, delay = 0) {
|
||||
if (scope.closed)
|
||||
return Promise.resolve(false);
|
||||
@@ -5297,17 +5306,17 @@ function beginSettingAdd(item) {
|
||||
}
|
||||
};
|
||||
}
|
||||
const states = /* @__PURE__ */ new WeakMap();
|
||||
const states$1 = /* @__PURE__ */ new WeakMap();
|
||||
function rememberSettingState(item, capture) {
|
||||
if (capture)
|
||||
states.set(item, capture);
|
||||
states$1.set(item, capture);
|
||||
else
|
||||
states.delete(item);
|
||||
states$1.delete(item);
|
||||
}
|
||||
function captureSettingItem(item) {
|
||||
const descriptors = new Map(Reflect.ownKeys(item).map((key) => [key, Object.getOwnPropertyDescriptor(item, key)]));
|
||||
const nodes = [];
|
||||
const state2 = states.get(item);
|
||||
const state2 = states$1.get(item);
|
||||
const restoreState = state2?.();
|
||||
for (const key of ["html", "icon", "tooltip"]) {
|
||||
const value = descriptors.get(key)?.value;
|
||||
@@ -6134,142 +6143,288 @@ class Storage {
|
||||
}
|
||||
}
|
||||
const css = ".art-video-player {\n --art-theme: #f00;\n --art-font-color: #fff;\n --art-background-color: #000;\n --art-text-shadow-color: rgba(0, 0, 0, 0.5);\n --art-transition-duration: 0.2s;\n --art-padding: 10px;\n --art-border-radius: 3px;\n --art-progress-height: 6px;\n --art-progress-color: rgba(255, 255, 255, 0.25);\n --art-progress-top-gap: 10px;\n --art-hover-color: rgba(255, 255, 255, 0.25);\n --art-loaded-color: rgba(255, 255, 255, 0.25);\n --art-state-size: 80px;\n --art-state-opacity: 0.8;\n --art-bottom-height: 100px;\n --art-bottom-offset: 20px;\n --art-bottom-gap: 5px;\n --art-highlight-width: 8px;\n --art-highlight-color: rgba(255, 255, 255, 0.5);\n --art-control-height: 46px;\n --art-control-opacity: 0.75;\n --art-control-icon-size: 36px;\n --art-control-icon-scale: 1.1;\n --art-volume-height: 120px;\n --art-volume-handle-size: 14px;\n --art-lock-size: 36px;\n --art-indicator-scale: 0;\n --art-indicator-size: 16px;\n --art-fullscreen-web-index: 9999;\n --art-settings-icon-size: 24px;\n --art-settings-max-height: 300px;\n --art-selector-max-height: 300px;\n --art-contextmenus-min-width: 250px;\n --art-subtitle-font-size: 20px;\n --art-subtitle-gap: 5px;\n --art-subtitle-bottom: 15px;\n --art-subtitle-border: #000;\n --art-widget-background: rgba(0, 0, 0, 0.85);\n --art-tip-background: rgba(0, 0, 0, 0.7);\n --art-scrollbar-size: 4px;\n --art-scrollbar-background: rgba(255, 255, 255, 0.25);\n --art-scrollbar-background-hover: rgba(255, 255, 255, 0.5);\n --art-mini-progress-height: 2px;\n}\n.art-bg-cover {\n background-position: center center;\n background-repeat: no-repeat;\n background-size: cover;\n}\n.art-bottom-gradient {\n background-image: linear-gradient(to top, #000, rgba(0, 0, 0, 0.4), transparent);\n background-repeat: repeat-x;\n background-position: center bottom;\n}\n.art-backdrop-filter {\n -webkit-backdrop-filter: saturate(180%) blur(20px);\n backdrop-filter: saturate(180%) blur(20px);\n background-color: rgba(0, 0, 0, 0.75) !important;\n}\n.art-truncate {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.art-video-player {\n position: relative;\n margin: 0 auto;\n width: 100%;\n height: 100%;\n outline: 0;\n zoom: 1;\n padding: 0;\n text-align: left;\n direction: ltr;\n font-size: 14px;\n line-height: 1.3;\n user-select: none;\n box-sizing: border-box;\n color: var(--art-font-color);\n background-color: var(--art-background-color);\n text-shadow: 0 0 2px var(--art-text-shadow-color);\n font-family: PingFang SC, Helvetica Neue, Microsoft YaHei, Roboto, Arial, sans-serif;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n -ms-touch-action: manipulation;\n touch-action: manipulation;\n -ms-high-contrast-adjust: none;\n}\n.art-video-player *,\n.art-video-player *::before,\n.art-video-player *::after {\n box-sizing: border-box;\n}\n.art-video-player ::-webkit-scrollbar {\n width: var(--art-scrollbar-size);\n height: var(--art-scrollbar-size);\n}\n.art-video-player ::-webkit-scrollbar-thumb {\n background-color: var(--art-scrollbar-background);\n}\n.art-video-player ::-webkit-scrollbar-thumb:hover {\n background-color: var(--art-scrollbar-background-hover);\n}\n.art-video-player img {\n max-width: 100%;\n vertical-align: top;\n}\n.art-video-player svg {\n fill: var(--art-font-color);\n}\n.art-video-player a {\n color: var(--art-font-color);\n text-decoration: none;\n}\n.art-icon {\n line-height: 1;\n display: flex;\n justify-content: center;\n align-items: center;\n}\n.art-video-player.art-backdrop .art-contextmenus,\n.art-video-player.art-backdrop .art-info,\n.art-video-player.art-backdrop .art-settings,\n.art-video-player.art-backdrop .art-layer-auto-playback,\n.art-video-player.art-backdrop .art-selector-list,\n.art-video-player.art-backdrop .art-volume-inner {\n -webkit-backdrop-filter: saturate(180%) blur(20px);\n backdrop-filter: saturate(180%) blur(20px);\n background-color: rgba(0, 0, 0, 0.75) !important;\n}\n.art-video {\n position: absolute;\n inset: 0;\n z-index: 10;\n width: 100%;\n height: 100%;\n}\n.art-poster {\n position: absolute;\n inset: 0;\n z-index: 11;\n width: 100%;\n height: 100%;\n background-position: center center;\n background-repeat: no-repeat;\n background-size: cover;\n pointer-events: none;\n}\n.art-video-player .art-subtitle {\n display: none;\n justify-content: center;\n align-items: center;\n flex-direction: column;\n position: absolute;\n z-index: 20;\n width: 100%;\n padding: 0 5%;\n text-align: center;\n pointer-events: none;\n gap: var(--art-subtitle-gap);\n bottom: var(--art-subtitle-bottom);\n font-size: var(--art-subtitle-font-size);\n transition: bottom var(--art-transition-duration) ease;\n text-shadow: var(--art-subtitle-border) 1px 0 1px, var(--art-subtitle-border) 0 1px 1px, var(--art-subtitle-border) -1px 0 1px, var(--art-subtitle-border) 0 -1px 1px, var(--art-subtitle-border) 1px 1px 1px,Line truncated
|
||||
class Subtitle extends Component {
|
||||
function parseSubtitle(buffer, option) {
|
||||
const text = new TextDecoder(option.encoding).decode(buffer);
|
||||
switch (option.type || getExt(option.url)) {
|
||||
case "srt":
|
||||
return { vtt: option.onVttLoad(srtToVtt(text)) };
|
||||
case "ass":
|
||||
return { vtt: option.onVttLoad(assToVtt(text)) };
|
||||
case "vtt":
|
||||
return { vtt: option.onVttLoad(text) };
|
||||
default:
|
||||
return void 0;
|
||||
}
|
||||
}
|
||||
class SubtitleRequest {
|
||||
constructor(parent) {
|
||||
this.scope = parent.child();
|
||||
this.cancelled = new Promise((resolve) => {
|
||||
this.scope.add(() => {
|
||||
resolve(void 0);
|
||||
});
|
||||
});
|
||||
this.signal = requestController(this.scope)?.signal;
|
||||
}
|
||||
get active() {
|
||||
return !this.scope.closed;
|
||||
}
|
||||
cancel() {
|
||||
this.scope.dispose();
|
||||
}
|
||||
async run(work) {
|
||||
if (!this.active)
|
||||
return void 0;
|
||||
try {
|
||||
return await Promise.race([work(), this.cancelled]);
|
||||
} finally {
|
||||
this.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
const states = /* @__PURE__ */ new WeakMap();
|
||||
function initSubtitleState(subtitle, art) {
|
||||
const scope = getScope(art).child();
|
||||
const state2 = { scope, revision: 0, render: 0, trackRevision: 0 };
|
||||
states.set(subtitle, state2);
|
||||
scope.add(() => {
|
||||
releaseSubtitleURL(state2);
|
||||
});
|
||||
return state2;
|
||||
}
|
||||
function subtitleState(subtitle) {
|
||||
return states.get(subtitle);
|
||||
}
|
||||
function beginSubtitleRequest(state2) {
|
||||
const previous = state2.request;
|
||||
const request = new SubtitleRequest(state2.scope);
|
||||
state2.request = request;
|
||||
state2.revision += 1;
|
||||
previous?.cancel();
|
||||
return request;
|
||||
}
|
||||
function releaseSubtitleURL(state2) {
|
||||
const url = state2.ownedURL;
|
||||
state2.ownedURL = void 0;
|
||||
if (url)
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
function renderSubtitle(subtitle) {
|
||||
const state2 = subtitleState(subtitle);
|
||||
if (state2.scope.closed)
|
||||
return;
|
||||
const revision = state2.revision;
|
||||
const render = ++state2.render;
|
||||
const active = () => !state2.scope.closed && state2.revision === revision && state2.render === render;
|
||||
const { art } = subtitle;
|
||||
const { $subtitle } = art.template;
|
||||
$subtitle.innerHTML = "";
|
||||
if (!subtitle.activeCues.length)
|
||||
return;
|
||||
art.emit("subtitleBeforeUpdate", subtitle.activeCues);
|
||||
if (!active())
|
||||
return;
|
||||
const html2 = subtitle.activeCues.map((cue, index) => cue.text.split(/\r?\n/).filter((line) => line.trim()).map((line) => `<div class="art-subtitle-line" data-group="${index}">
|
||||
${art.option.subtitle.escape ? escape(line) : line}
|
||||
</div>`).join("")).join("");
|
||||
if (!active())
|
||||
return;
|
||||
$subtitle.innerHTML = html2;
|
||||
art.emit("subtitleAfterUpdate", subtitle.activeCues);
|
||||
}
|
||||
function replaceSubtitleTrack(subtitle, kind, url) {
|
||||
const state2 = subtitleState(subtitle);
|
||||
if (state2.scope.closed)
|
||||
return;
|
||||
const revision = ++state2.trackRevision;
|
||||
const { art } = subtitle;
|
||||
const { template } = art;
|
||||
const previous = template.$track;
|
||||
const parent = previous.parentNode;
|
||||
const next = previous.nextSibling;
|
||||
const track = document.createElement("track");
|
||||
const scope = state2.scope.child();
|
||||
scope.add(() => {
|
||||
track.onload = null;
|
||||
track.onerror = null;
|
||||
});
|
||||
let committed = false;
|
||||
try {
|
||||
track.default = true;
|
||||
track.kind = kind;
|
||||
track.src = url;
|
||||
track.label = art.option.subtitle.name || "Artplayer";
|
||||
track.track.mode = "hidden";
|
||||
track.onload = () => {
|
||||
if (!scope.closed && template.$track === track)
|
||||
art.emit("subtitleLoad", subtitle.cues, subtitle.option);
|
||||
};
|
||||
track.onerror = () => {
|
||||
if (!scope.closed && template.$track === track)
|
||||
art.notice.show = new Error("Failed to load subtitle track");
|
||||
};
|
||||
if (scope.closed || state2.trackRevision !== revision) {
|
||||
scope.dispose();
|
||||
return;
|
||||
}
|
||||
previous.remove();
|
||||
template.$video.appendChild(track);
|
||||
template.$track = track;
|
||||
const textTrack = subtitle.textTrack;
|
||||
const cleanup = textTrack ? art.proxy(textTrack, "cuechange", () => {
|
||||
if (!scope.closed && template.$track === track)
|
||||
subtitle.update();
|
||||
}) : () => {
|
||||
};
|
||||
scope.add(() => {
|
||||
art.events.remove(cleanup);
|
||||
});
|
||||
if (scope.closed || state2.trackRevision !== revision) {
|
||||
scope.dispose();
|
||||
track.remove();
|
||||
return;
|
||||
}
|
||||
const previousScope = state2.track;
|
||||
state2.track = scope;
|
||||
subtitle.destroyEvent = cleanup;
|
||||
previous.onload = null;
|
||||
previous.onerror = null;
|
||||
committed = true;
|
||||
try {
|
||||
previousScope?.dispose();
|
||||
} finally {
|
||||
if (state2.ownedURL && state2.ownedURL !== url)
|
||||
releaseSubtitleURL(state2);
|
||||
}
|
||||
} catch (error2) {
|
||||
if (committed)
|
||||
throw error2;
|
||||
scope.dispose();
|
||||
track.remove();
|
||||
if (state2.scope.closed || state2.trackRevision !== revision)
|
||||
throw error2;
|
||||
template.$track = previous;
|
||||
if (parent)
|
||||
parent.insertBefore(previous, next?.parentNode === parent ? next : null);
|
||||
throw error2;
|
||||
}
|
||||
}
|
||||
const SubtitleBase = Component;
|
||||
class Subtitle extends SubtitleBase {
|
||||
constructor(art) {
|
||||
super(art);
|
||||
this.name = "subtitle";
|
||||
this.option = null;
|
||||
this.destroyEvent = () => null;
|
||||
this.init(art.option.subtitle);
|
||||
const state2 = initSubtitleState(this, art);
|
||||
void this.init(art.option.subtitle).catch(() => {
|
||||
});
|
||||
let lastState = false;
|
||||
art.on("video:timeupdate", () => {
|
||||
if (!this.url)
|
||||
const timeupdate = () => {
|
||||
if (state2.scope.closed || !this.url)
|
||||
return;
|
||||
const state2 = this.art.template.$video.webkitDisplayingFullscreen;
|
||||
if (typeof state2 !== "boolean")
|
||||
return;
|
||||
if (state2 !== lastState) {
|
||||
lastState = state2;
|
||||
this.createTrack(state2 ? "subtitles" : "metadata", this.url);
|
||||
const fullscreen2 = art.template.$video.webkitDisplayingFullscreen;
|
||||
if (typeof fullscreen2 === "boolean" && fullscreen2 !== lastState) {
|
||||
lastState = fullscreen2;
|
||||
this.createTrack(fullscreen2 ? "subtitles" : "metadata", this.url);
|
||||
}
|
||||
};
|
||||
art.on("video:timeupdate", timeupdate);
|
||||
state2.scope.add(() => {
|
||||
art.off("video:timeupdate", timeupdate);
|
||||
});
|
||||
}
|
||||
get url() {
|
||||
return this.art.template.$track.src;
|
||||
}
|
||||
set url(url) {
|
||||
this.switch(url);
|
||||
void this.switch(url).catch(() => {
|
||||
});
|
||||
}
|
||||
get textTrack() {
|
||||
return this.art.template.$video?.textTracks?.[0];
|
||||
}
|
||||
get activeCues() {
|
||||
if (!this.textTrack)
|
||||
return [];
|
||||
return Array.from(this.textTrack.activeCues);
|
||||
return Array.from(this.textTrack?.activeCues || []);
|
||||
}
|
||||
get cues() {
|
||||
if (!this.textTrack)
|
||||
return [];
|
||||
return Array.from(this.textTrack.cues);
|
||||
return Array.from(this.textTrack?.cues || []);
|
||||
}
|
||||
style(key, value) {
|
||||
const { $subtitle } = this.art.template;
|
||||
if (typeof key === "object") {
|
||||
return setStyles($subtitle, key);
|
||||
}
|
||||
return setStyle($subtitle, key, value);
|
||||
return typeof key === "object" ? setStyles($subtitle, key) : setStyle($subtitle, key, value);
|
||||
}
|
||||
update() {
|
||||
const {
|
||||
option: { subtitle },
|
||||
template: { $subtitle }
|
||||
} = this.art;
|
||||
$subtitle.innerHTML = "";
|
||||
if (!this.activeCues.length)
|
||||
return;
|
||||
this.art.emit("subtitleBeforeUpdate", this.activeCues);
|
||||
$subtitle.innerHTML = this.activeCues.map(
|
||||
(cue, index) => cue.text.split(/\r?\n/).filter((line) => line.trim()).map(
|
||||
(line) => `<div class="art-subtitle-line" data-group="${index}">
|
||||
${subtitle.escape ? escape(line) : line}
|
||||
</div>`
|
||||
).join("")
|
||||
).join("");
|
||||
this.art.emit("subtitleAfterUpdate", this.activeCues);
|
||||
renderSubtitle(this);
|
||||
}
|
||||
async switch(url, newOption = {}) {
|
||||
const { i18n, notice, option } = this.art;
|
||||
const subtitleOption = { ...option.subtitle, ...newOption, url };
|
||||
const state2 = subtitleState(this);
|
||||
const revision = state2.revision + 1;
|
||||
const subUrl = await this.init(subtitleOption);
|
||||
if (newOption.name) {
|
||||
if (!state2.scope.closed && state2.revision === revision && newOption.name)
|
||||
notice.show = `${i18n.get("Switch Subtitle")}: ${newOption.name}`;
|
||||
}
|
||||
return subUrl;
|
||||
}
|
||||
createTrack(kind, url) {
|
||||
const { template, proxy, option } = this.art;
|
||||
const { $video, $track } = template;
|
||||
const $newTrack = createElement("track");
|
||||
$newTrack.default = true;
|
||||
$newTrack.kind = kind;
|
||||
$newTrack.src = url;
|
||||
$newTrack.label = option.subtitle.name || "Artplayer";
|
||||
$newTrack.track.mode = "hidden";
|
||||
$newTrack.onload = () => {
|
||||
this.art.emit("subtitleLoad", this.cues, this.option);
|
||||
};
|
||||
this.art.events.remove(this.destroyEvent);
|
||||
$track.onload = null;
|
||||
remove($track);
|
||||
append($video, $newTrack);
|
||||
template.$track = $newTrack;
|
||||
this.destroyEvent = proxy(this.textTrack, "cuechange", () => this.update());
|
||||
replaceSubtitleTrack(this, kind, url);
|
||||
}
|
||||
async init(subtitleOption) {
|
||||
const {
|
||||
notice,
|
||||
template: { $subtitle }
|
||||
} = this.art;
|
||||
if (!this.textTrack)
|
||||
return null;
|
||||
validator(subtitleOption, scheme.subtitle);
|
||||
if (!subtitleOption.url)
|
||||
return;
|
||||
this.option = subtitleOption;
|
||||
this.style(subtitleOption.style);
|
||||
return fetch(subtitleOption.url).then((response) => response.arrayBuffer()).then((buffer) => {
|
||||
const decoder = new TextDecoder(subtitleOption.encoding);
|
||||
const text = decoder.decode(buffer);
|
||||
switch (subtitleOption.type || getExt(subtitleOption.url)) {
|
||||
case "srt": {
|
||||
const vtt = srtToVtt(text);
|
||||
const vttNew = subtitleOption.onVttLoad(vtt);
|
||||
return vttToBlob(vttNew);
|
||||
const state2 = subtitleState(this);
|
||||
const request = beginSubtitleRequest(state2);
|
||||
const { notice, template: { $subtitle } } = this.art;
|
||||
return request.run(async () => {
|
||||
if (!this.textTrack)
|
||||
return null;
|
||||
validator(subtitleOption, scheme.subtitle);
|
||||
if (!subtitleOption.url)
|
||||
return void 0;
|
||||
this.option = subtitleOption;
|
||||
this.style(subtitleOption.style);
|
||||
let generated;
|
||||
try {
|
||||
if (!request.active)
|
||||
return void 0;
|
||||
const response = await fetch(subtitleOption.url, { signal: request.signal });
|
||||
if (!request.active)
|
||||
return void 0;
|
||||
if (!response.ok)
|
||||
throw new Error(`Failed to load subtitle: ${response.status} ${response.statusText}`);
|
||||
const buffer = await response.arrayBuffer();
|
||||
if (!request.active)
|
||||
return void 0;
|
||||
const vtt = parseSubtitle(buffer, subtitleOption);
|
||||
if (!request.active)
|
||||
return void 0;
|
||||
const subUrl = vtt === void 0 ? subtitleOption.url : generated = vttToBlob(vtt.vtt);
|
||||
if (!request.active)
|
||||
return void 0;
|
||||
$subtitle.innerHTML = "";
|
||||
const trackRevision = state2.trackRevision + 1;
|
||||
if (this.url !== subUrl)
|
||||
this.createTrack("metadata", subUrl);
|
||||
if (!request.active || state2.trackRevision > trackRevision)
|
||||
return void 0;
|
||||
if (generated) {
|
||||
state2.ownedURL = generated;
|
||||
generated = void 0;
|
||||
}
|
||||
case "ass": {
|
||||
const vtt = assToVtt(text);
|
||||
const vttNew = subtitleOption.onVttLoad(vtt);
|
||||
return vttToBlob(vttNew);
|
||||
}
|
||||
case "vtt": {
|
||||
const vttNew = subtitleOption.onVttLoad(text);
|
||||
return vttToBlob(vttNew);
|
||||
}
|
||||
default:
|
||||
return subtitleOption.url;
|
||||
}
|
||||
}).then((subUrl) => {
|
||||
$subtitle.innerHTML = "";
|
||||
if (this.url === subUrl)
|
||||
return subUrl;
|
||||
URL.revokeObjectURL(this.url);
|
||||
this.createTrack("metadata", subUrl);
|
||||
return subUrl;
|
||||
}).catch((err) => {
|
||||
$subtitle.innerHTML = "";
|
||||
notice.show = err;
|
||||
throw err;
|
||||
} catch (error2) {
|
||||
if (!request.active)
|
||||
return void 0;
|
||||
$subtitle.innerHTML = "";
|
||||
notice.show = error2;
|
||||
throw error2;
|
||||
} finally {
|
||||
if (generated) {
|
||||
if (!state2.scope.closed && this.url === generated)
|
||||
state2.ownedURL = generated;
|
||||
else
|
||||
URL.revokeObjectURL(generated);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+4
-3
@@ -1,20 +1,21 @@
|
||||
import type { SubtitleOffsetHost } from '../subtitle/types'
|
||||
import { clamp, def } from '../utils'
|
||||
|
||||
export default function subtitleOffsetMix(art) {
|
||||
export default function subtitleOffsetMix(art: SubtitleOffsetHost): void {
|
||||
const { notice, i18n, template } = art
|
||||
|
||||
def(art, 'subtitleOffset', {
|
||||
get() {
|
||||
return template.$track?.offset || 0
|
||||
},
|
||||
set(value) {
|
||||
set(value: number) {
|
||||
const { cues } = art.subtitle
|
||||
if (!template.$track || cues.length === 0)
|
||||
return
|
||||
const offset = clamp(value, -10, 10)
|
||||
template.$track.offset = offset
|
||||
for (let index = 0; index < cues.length; index++) {
|
||||
const cue = cues[index]
|
||||
const cue = cues[index]!
|
||||
cue.originalStartTime = cue.originalStartTime ?? cue.startTime
|
||||
cue.originalEndTime = cue.originalEndTime ?? cue.endTime
|
||||
cue.startTime = clamp(cue.originalStartTime + offset, 0, art.duration)
|
||||
@@ -1,183 +0,0 @@
|
||||
import validator from 'option-validator'
|
||||
import scheme from './scheme'
|
||||
import {
|
||||
append,
|
||||
assToVtt,
|
||||
createElement,
|
||||
escape,
|
||||
getExt,
|
||||
remove,
|
||||
setStyle,
|
||||
setStyles,
|
||||
srtToVtt,
|
||||
vttToBlob,
|
||||
} from './utils'
|
||||
import Component from './utils/component'
|
||||
|
||||
export default class Subtitle extends Component {
|
||||
constructor(art) {
|
||||
super(art)
|
||||
this.name = 'subtitle'
|
||||
this.option = null
|
||||
this.destroyEvent = () => null
|
||||
this.init(art.option.subtitle)
|
||||
|
||||
let lastState = false
|
||||
art.on('video:timeupdate', () => {
|
||||
if (!this.url)
|
||||
return
|
||||
const state = this.art.template.$video.webkitDisplayingFullscreen
|
||||
if (typeof state !== 'boolean')
|
||||
return
|
||||
if (state !== lastState) {
|
||||
lastState = state
|
||||
this.createTrack(state ? 'subtitles' : 'metadata', this.url)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
get url() {
|
||||
return this.art.template.$track.src
|
||||
}
|
||||
|
||||
set url(url) {
|
||||
this.switch(url)
|
||||
}
|
||||
|
||||
get textTrack() {
|
||||
return this.art.template.$video?.textTracks?.[0]
|
||||
}
|
||||
|
||||
get activeCues() {
|
||||
if (!this.textTrack)
|
||||
return []
|
||||
return Array.from(this.textTrack.activeCues)
|
||||
}
|
||||
|
||||
get cues() {
|
||||
if (!this.textTrack)
|
||||
return []
|
||||
return Array.from(this.textTrack.cues)
|
||||
}
|
||||
|
||||
style(key, value) {
|
||||
const { $subtitle } = this.art.template
|
||||
if (typeof key === 'object') {
|
||||
return setStyles($subtitle, key)
|
||||
}
|
||||
return setStyle($subtitle, key, value)
|
||||
}
|
||||
|
||||
update() {
|
||||
const {
|
||||
option: { subtitle },
|
||||
template: { $subtitle },
|
||||
} = this.art
|
||||
|
||||
$subtitle.innerHTML = ''
|
||||
if (!this.activeCues.length)
|
||||
return
|
||||
|
||||
this.art.emit('subtitleBeforeUpdate', this.activeCues)
|
||||
$subtitle.innerHTML = this.activeCues
|
||||
.map((cue, index) =>
|
||||
cue.text
|
||||
.split(/\r?\n/)
|
||||
.filter(line => line.trim())
|
||||
.map(
|
||||
line =>
|
||||
`<div class="art-subtitle-line" data-group="${index}">
|
||||
${subtitle.escape ? escape(line) : line}
|
||||
</div>`,
|
||||
)
|
||||
.join(''),
|
||||
)
|
||||
.join('')
|
||||
this.art.emit('subtitleAfterUpdate', this.activeCues)
|
||||
}
|
||||
|
||||
async switch(url, newOption = {}) {
|
||||
const { i18n, notice, option } = this.art
|
||||
const subtitleOption = { ...option.subtitle, ...newOption, url }
|
||||
const subUrl = await this.init(subtitleOption)
|
||||
if (newOption.name) {
|
||||
notice.show = `${i18n.get('Switch Subtitle')}: ${newOption.name}`
|
||||
}
|
||||
return subUrl
|
||||
}
|
||||
|
||||
createTrack(kind, url) {
|
||||
const { template, proxy, option } = this.art
|
||||
const { $video, $track } = template
|
||||
|
||||
const $newTrack = createElement('track')
|
||||
$newTrack.default = true
|
||||
$newTrack.kind = kind
|
||||
$newTrack.src = url
|
||||
$newTrack.label = option.subtitle.name || 'Artplayer'
|
||||
$newTrack.track.mode = 'hidden'
|
||||
$newTrack.onload = () => {
|
||||
this.art.emit('subtitleLoad', this.cues, this.option)
|
||||
}
|
||||
|
||||
this.art.events.remove(this.destroyEvent)
|
||||
$track.onload = null
|
||||
remove($track)
|
||||
append($video, $newTrack)
|
||||
template.$track = $newTrack
|
||||
this.destroyEvent = proxy(this.textTrack, 'cuechange', () => this.update())
|
||||
}
|
||||
|
||||
async init(subtitleOption) {
|
||||
const {
|
||||
notice,
|
||||
template: { $subtitle },
|
||||
} = this.art
|
||||
|
||||
if (!this.textTrack)
|
||||
return null
|
||||
validator(subtitleOption, scheme.subtitle)
|
||||
if (!subtitleOption.url)
|
||||
return
|
||||
this.option = subtitleOption
|
||||
this.style(subtitleOption.style)
|
||||
|
||||
return fetch(subtitleOption.url)
|
||||
.then(response => response.arrayBuffer())
|
||||
.then((buffer) => {
|
||||
const decoder = new TextDecoder(subtitleOption.encoding)
|
||||
const text = decoder.decode(buffer)
|
||||
switch (subtitleOption.type || getExt(subtitleOption.url)) {
|
||||
case 'srt': {
|
||||
const vtt = srtToVtt(text)
|
||||
const vttNew = subtitleOption.onVttLoad(vtt)
|
||||
return vttToBlob(vttNew)
|
||||
}
|
||||
case 'ass': {
|
||||
const vtt = assToVtt(text)
|
||||
const vttNew = subtitleOption.onVttLoad(vtt)
|
||||
return vttToBlob(vttNew)
|
||||
}
|
||||
case 'vtt': {
|
||||
const vttNew = subtitleOption.onVttLoad(text)
|
||||
return vttToBlob(vttNew)
|
||||
}
|
||||
default:
|
||||
return subtitleOption.url
|
||||
}
|
||||
})
|
||||
.then((subUrl) => {
|
||||
$subtitle.innerHTML = ''
|
||||
if (this.url === subUrl)
|
||||
return subUrl
|
||||
URL.revokeObjectURL(this.url)
|
||||
this.createTrack('metadata', subUrl)
|
||||
return subUrl
|
||||
})
|
||||
.catch((err) => {
|
||||
$subtitle.innerHTML = ''
|
||||
notice.show = err
|
||||
throw err
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import type { Subtitle as SubtitleInput } from '../../types/subtitle'
|
||||
import type { SubtitleCue, SubtitleHost, SubtitleOption } from './types'
|
||||
import validator from 'option-validator'
|
||||
import scheme from '../scheme'
|
||||
import { setStyle, setStyles, vttToBlob } from '../utils'
|
||||
import Component from '../utils/component'
|
||||
import { parseSubtitle } from './parse'
|
||||
import { renderSubtitle } from './render'
|
||||
import { beginSubtitleRequest, initSubtitleState, subtitleState } from './state'
|
||||
import { replaceSubtitleTrack } from './track'
|
||||
|
||||
// Retain Component's runtime prototype and method binding without its registry update type.
|
||||
const SubtitleBase: new (art: SubtitleHost) => Omit<Component<SubtitleHost>, 'update'> = Component
|
||||
|
||||
export default class Subtitle extends SubtitleBase {
|
||||
declare name: string
|
||||
declare option: SubtitleOption | null
|
||||
declare destroyEvent: () => unknown
|
||||
|
||||
constructor(art: SubtitleHost) {
|
||||
super(art)
|
||||
this.name = 'subtitle'
|
||||
this.option = null
|
||||
this.destroyEvent = () => null
|
||||
const state = initSubtitleState(this, art)
|
||||
// init reports active failures through notice; construction has no promise consumer.
|
||||
void this.init(art.option.subtitle).catch(() => {})
|
||||
let lastState = false
|
||||
const timeupdate = () => {
|
||||
if (state.scope.closed || !this.url)
|
||||
return
|
||||
const fullscreen = art.template.$video.webkitDisplayingFullscreen
|
||||
if (typeof fullscreen === 'boolean' && fullscreen !== lastState) {
|
||||
lastState = fullscreen
|
||||
this.createTrack(fullscreen ? 'subtitles' : 'metadata', this.url)
|
||||
}
|
||||
}
|
||||
art.on('video:timeupdate', timeupdate)
|
||||
state.scope.add(() => {
|
||||
art.off('video:timeupdate', timeupdate)
|
||||
})
|
||||
}
|
||||
|
||||
get url(): string {
|
||||
return this.art.template.$track.src
|
||||
}
|
||||
|
||||
set url(url: string) {
|
||||
void this.switch(url).catch(() => {})
|
||||
}
|
||||
|
||||
get textTrack(): TextTrack | undefined {
|
||||
return this.art.template.$video?.textTracks?.[0]
|
||||
}
|
||||
|
||||
get activeCues(): SubtitleCue[] {
|
||||
// ArtPlayer's converted resources are WebVTT; disabled tracks expose null cue lists.
|
||||
return Array.from(this.textTrack?.activeCues || []) as SubtitleCue[]
|
||||
}
|
||||
|
||||
get cues(): SubtitleCue[] {
|
||||
return Array.from(this.textTrack?.cues || []) as SubtitleCue[]
|
||||
}
|
||||
|
||||
style(styles: Partial<CSSStyleDeclaration>): HTMLDivElement
|
||||
style(key: string, value: string | number): HTMLDivElement
|
||||
style(key: string | Partial<CSSStyleDeclaration>, value?: string | number): HTMLDivElement {
|
||||
const { $subtitle } = this.art.template
|
||||
return typeof key === 'object' ? setStyles($subtitle, key) : setStyle($subtitle, key, value)
|
||||
}
|
||||
|
||||
update(): void {
|
||||
renderSubtitle(this)
|
||||
}
|
||||
|
||||
async switch(url: string, newOption: SubtitleInput = {}): Promise<string | null | undefined> {
|
||||
const { i18n, notice, option } = this.art
|
||||
const subtitleOption = { ...option.subtitle, ...newOption, url }
|
||||
const state = subtitleState(this)
|
||||
const revision = state.revision + 1
|
||||
const subUrl = await this.init(subtitleOption)
|
||||
if (!state.scope.closed && state.revision === revision && newOption.name)
|
||||
notice.show = `${i18n.get('Switch Subtitle')}: ${newOption.name}`
|
||||
return subUrl
|
||||
}
|
||||
|
||||
createTrack(kind: string, url: string): void {
|
||||
replaceSubtitleTrack(this, kind, url)
|
||||
}
|
||||
|
||||
async init(subtitleOption: SubtitleOption): Promise<string | null | undefined> {
|
||||
const state = subtitleState(this)
|
||||
const request = beginSubtitleRequest(state)
|
||||
const { notice, template: { $subtitle } } = this.art
|
||||
return request.run(async () => {
|
||||
if (!this.textTrack)
|
||||
return null
|
||||
validator(subtitleOption, scheme.subtitle)
|
||||
if (!subtitleOption.url)
|
||||
return undefined
|
||||
this.option = subtitleOption
|
||||
this.style(subtitleOption.style)
|
||||
let generated: string | undefined
|
||||
try {
|
||||
if (!request.active)
|
||||
return undefined
|
||||
const response = await fetch(subtitleOption.url, { signal: request.signal })
|
||||
if (!request.active)
|
||||
return undefined
|
||||
if (!response.ok)
|
||||
throw new Error(`Failed to load subtitle: ${response.status} ${response.statusText}`)
|
||||
const buffer = await response.arrayBuffer()
|
||||
if (!request.active)
|
||||
return undefined
|
||||
const vtt = parseSubtitle(buffer, subtitleOption)
|
||||
if (!request.active)
|
||||
return undefined
|
||||
const subUrl = vtt === undefined ? subtitleOption.url : (generated = vttToBlob(vtt.vtt))
|
||||
if (!request.active)
|
||||
return undefined
|
||||
$subtitle.innerHTML = ''
|
||||
const trackRevision = state.trackRevision + 1
|
||||
if (this.url !== subUrl)
|
||||
this.createTrack('metadata', subUrl)
|
||||
if (!request.active || state.trackRevision > trackRevision)
|
||||
return undefined
|
||||
if (generated) {
|
||||
state.ownedURL = generated
|
||||
generated = undefined
|
||||
}
|
||||
return subUrl
|
||||
}
|
||||
catch (error) {
|
||||
if (!request.active)
|
||||
return undefined
|
||||
$subtitle.innerHTML = ''
|
||||
notice.show = error
|
||||
throw error
|
||||
}
|
||||
finally {
|
||||
if (generated) {
|
||||
// A replacement can commit before an old listener cleanup throws.
|
||||
if (!state.scope.closed && this.url === generated)
|
||||
state.ownedURL = generated
|
||||
else
|
||||
URL.revokeObjectURL(generated)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { SubtitleOption } from './types'
|
||||
import { getExt } from '../utils'
|
||||
import { assToVtt, srtToVtt } from '../utils/subtitle'
|
||||
|
||||
// Keep conversion and callback binding identical to the public utility parsers.
|
||||
export function parseSubtitle(buffer: ArrayBuffer, option: SubtitleOption): { vtt: string } | undefined {
|
||||
const text = new TextDecoder(option.encoding).decode(buffer)
|
||||
switch (option.type || getExt(option.url)) {
|
||||
case 'srt': return { vtt: option.onVttLoad(srtToVtt(text)) }
|
||||
case 'ass': return { vtt: option.onVttLoad(assToVtt(text)) }
|
||||
case 'vtt': return { vtt: option.onVttLoad(text) }
|
||||
default: return undefined
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { SubtitleView } from './types'
|
||||
import { escape } from '../utils'
|
||||
import { subtitleState } from './state'
|
||||
|
||||
export function renderSubtitle(subtitle: SubtitleView): void {
|
||||
const state = subtitleState(subtitle)
|
||||
if (state.scope.closed)
|
||||
return
|
||||
const revision = state.revision
|
||||
const render = ++state.render
|
||||
const active = () => !state.scope.closed && state.revision === revision && state.render === render
|
||||
const { art } = subtitle
|
||||
const { $subtitle } = art.template
|
||||
$subtitle.innerHTML = ''
|
||||
if (!subtitle.activeCues.length)
|
||||
return
|
||||
art.emit('subtitleBeforeUpdate', subtitle.activeCues)
|
||||
if (!active())
|
||||
return
|
||||
const html = subtitle.activeCues.map((cue, index) => cue.text
|
||||
.split(/\r?\n/)
|
||||
.filter(line => line.trim())
|
||||
.map(line => `<div class="art-subtitle-line" data-group="${index}">
|
||||
${art.option.subtitle.escape ? escape(line) : line}
|
||||
</div>`)
|
||||
.join('')).join('')
|
||||
if (!active())
|
||||
return
|
||||
$subtitle.innerHTML = html
|
||||
art.emit('subtitleAfterUpdate', subtitle.activeCues)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import type ResourceScope from '../lifecycle/scope'
|
||||
import { requestController } from '../lifecycle/resources'
|
||||
|
||||
// Cancellation settles callers even when a fetch adapter ignores AbortSignal.
|
||||
export default class SubtitleRequest {
|
||||
readonly scope: ResourceScope
|
||||
readonly signal?: AbortSignal
|
||||
private readonly cancelled: Promise<undefined>
|
||||
|
||||
constructor(parent: ResourceScope) {
|
||||
this.scope = parent.child()
|
||||
this.cancelled = new Promise((resolve) => {
|
||||
this.scope.add(() => {
|
||||
resolve(undefined)
|
||||
})
|
||||
})
|
||||
this.signal = requestController(this.scope)?.signal
|
||||
}
|
||||
|
||||
get active(): boolean {
|
||||
return !this.scope.closed
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
this.scope.dispose()
|
||||
}
|
||||
|
||||
async run<Result>(work: () => Promise<Result>): Promise<Result | undefined> {
|
||||
if (!this.active)
|
||||
return undefined
|
||||
try {
|
||||
return await Promise.race([work(), this.cancelled])
|
||||
}
|
||||
finally {
|
||||
this.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import type ResourceScope from '../lifecycle/scope'
|
||||
import { getScope } from '../lifecycle/instance'
|
||||
import SubtitleRequest from './request'
|
||||
|
||||
export interface SubtitleState {
|
||||
scope: ResourceScope
|
||||
request?: SubtitleRequest
|
||||
track?: ResourceScope
|
||||
ownedURL?: string
|
||||
revision: number
|
||||
render: number
|
||||
trackRevision: number
|
||||
}
|
||||
|
||||
const states = new WeakMap<object, SubtitleState>()
|
||||
|
||||
export function initSubtitleState(subtitle: object, art: object): SubtitleState {
|
||||
const scope = getScope(art).child()
|
||||
const state: SubtitleState = { scope, revision: 0, render: 0, trackRevision: 0 }
|
||||
states.set(subtitle, state)
|
||||
scope.add(() => {
|
||||
releaseSubtitleURL(state)
|
||||
})
|
||||
return state
|
||||
}
|
||||
|
||||
export function subtitleState(subtitle: object): SubtitleState {
|
||||
return states.get(subtitle)!
|
||||
}
|
||||
|
||||
export function beginSubtitleRequest(state: SubtitleState): SubtitleRequest {
|
||||
const previous = state.request
|
||||
const request = new SubtitleRequest(state.scope)
|
||||
state.request = request
|
||||
state.revision += 1
|
||||
previous?.cancel()
|
||||
return request
|
||||
}
|
||||
|
||||
export function releaseSubtitleURL(state: SubtitleState): void {
|
||||
const url = state.ownedURL
|
||||
state.ownedURL = undefined
|
||||
if (url)
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { SubtitleView } from './types'
|
||||
import { releaseSubtitleURL, subtitleState } from './state'
|
||||
|
||||
export function replaceSubtitleTrack(subtitle: SubtitleView, kind: string, url: string): void {
|
||||
const state = subtitleState(subtitle)
|
||||
if (state.scope.closed)
|
||||
return
|
||||
const revision = ++state.trackRevision
|
||||
const { art } = subtitle
|
||||
const { template } = art
|
||||
const previous = template.$track
|
||||
const parent = previous.parentNode
|
||||
const next = previous.nextSibling
|
||||
const track = document.createElement('track')
|
||||
const scope = state.scope.child()
|
||||
scope.add(() => {
|
||||
track.onload = null
|
||||
track.onerror = null
|
||||
})
|
||||
let committed = false
|
||||
try {
|
||||
track.default = true
|
||||
track.kind = kind
|
||||
track.src = url
|
||||
track.label = art.option.subtitle.name || 'Artplayer'
|
||||
track.track.mode = 'hidden'
|
||||
track.onload = () => {
|
||||
if (!scope.closed && template.$track === track)
|
||||
art.emit('subtitleLoad', subtitle.cues, subtitle.option)
|
||||
}
|
||||
track.onerror = () => {
|
||||
if (!scope.closed && template.$track === track)
|
||||
art.notice.show = new Error('Failed to load subtitle track')
|
||||
}
|
||||
if (scope.closed || state.trackRevision !== revision) {
|
||||
scope.dispose()
|
||||
return
|
||||
}
|
||||
previous.remove()
|
||||
template.$video.appendChild(track)
|
||||
template.$track = track
|
||||
const textTrack = subtitle.textTrack
|
||||
const cleanup = textTrack
|
||||
? art.proxy(textTrack, 'cuechange', () => {
|
||||
if (!scope.closed && template.$track === track)
|
||||
subtitle.update()
|
||||
})
|
||||
: () => {}
|
||||
scope.add(() => {
|
||||
art.events.remove(cleanup)
|
||||
})
|
||||
if (scope.closed || state.trackRevision !== revision) {
|
||||
scope.dispose()
|
||||
track.remove()
|
||||
return
|
||||
}
|
||||
const previousScope = state.track
|
||||
state.track = scope
|
||||
subtitle.destroyEvent = cleanup
|
||||
previous.onload = null
|
||||
previous.onerror = null
|
||||
committed = true
|
||||
try {
|
||||
previousScope?.dispose()
|
||||
}
|
||||
finally {
|
||||
if (state.ownedURL && state.ownedURL !== url)
|
||||
releaseSubtitleURL(state)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
if (committed)
|
||||
throw error
|
||||
scope.dispose()
|
||||
track.remove()
|
||||
if (state.scope.closed || state.trackRevision !== revision)
|
||||
throw error
|
||||
template.$track = previous
|
||||
if (parent)
|
||||
parent.insertBefore(previous, next?.parentNode === parent ? next : null)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { Subtitle as SubtitleInput } from '../../types/subtitle'
|
||||
import type { ComponentHost } from '../component/types'
|
||||
import type { OptionalMediaCapabilities } from '../media/types'
|
||||
|
||||
export type SubtitleOption = Required<SubtitleInput>
|
||||
|
||||
export interface SubtitleCue extends TextTrackCue {
|
||||
text: string
|
||||
originalStartTime?: number
|
||||
originalEndTime?: number
|
||||
}
|
||||
|
||||
export interface SubtitleTrack extends HTMLTrackElement {
|
||||
offset?: number
|
||||
}
|
||||
|
||||
export interface SubtitleHost extends ComponentHost {
|
||||
template: { $player: HTMLElement, $video: HTMLElement & OptionalMediaCapabilities, $subtitle: HTMLDivElement, $track: SubtitleTrack }
|
||||
option: { subtitle: SubtitleOption }
|
||||
proxy: ComponentHost['events']['proxy']
|
||||
on: (name: 'video:timeupdate', callback: () => void) => unknown
|
||||
off: (name: 'video:timeupdate', callback: () => void) => unknown
|
||||
notice: { set show(value: unknown) }
|
||||
i18n: { get: (key: string) => string }
|
||||
}
|
||||
|
||||
export interface SubtitleView {
|
||||
art: SubtitleHost
|
||||
option: SubtitleOption | null
|
||||
destroyEvent: () => unknown
|
||||
readonly cues: SubtitleCue[]
|
||||
readonly activeCues: SubtitleCue[]
|
||||
readonly textTrack: TextTrack | undefined
|
||||
update: () => void
|
||||
}
|
||||
|
||||
export interface SubtitleOffsetHost {
|
||||
template: { $track?: SubtitleTrack | null }
|
||||
subtitle: { readonly cues: SubtitleCue[], update: () => void }
|
||||
readonly duration: number
|
||||
notice: { set show(value: string) }
|
||||
i18n: { get: (key: string) => string }
|
||||
emit: (name: 'subtitleOffset', value: number) => unknown
|
||||
}
|
||||
Reference in new issue
Block a user