mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 10:56:15 -08:00
refactor(core): CORE-16 type display modes and own fullscreen lifetimes
This commit is contained in:
1 parent
1b83081e8f
commit
88aef830df
81 files changed
+33897
-1503
No files matched your search
@@ -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 n=(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 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}()),r.exports);const o=t(n),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
|
||||
@@ -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,i=Object.prototype.hasOwnProperty,a=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={}))i.call(e,r)&&s(t,r,e[r]);if(o)for(var r of o(e))a.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 i=t=>{try{s(r.next(t))}catch(e){o(e)}},a=t=>{try{s(r.throw(t))}catch(e){o(e)}},s=t=>t.done?n(t.value):Promise.resolve(t.value).then(i,a);s((r=r.apply(t,e)).next())});function u(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var p,h={exports:{}};var f=(p||(p=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 i(t,e,n),a(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),i(s,l,c),a(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),i(l,c,d),a(l,c,d),o(l,c,d)})}}(t,e,n),t}function i(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 a(t,e,n){if("function"===r(e)){var o=e(t,r(t),n);if(!0!==o){var i=r(o);throw"string"===i?new Error(o):"error"===i?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=u(f),g="5.4.1",v={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","meLine truncated
|
||||
+1200
-460
File diff suppressed because it is too large.
Load diff
+1
-1
@@ -49,7 +49,7 @@
|
||||
"ci:build": "yarn build all && yarn build:i18n && yarn build:ts && yarn build:docs && yarn test:imports",
|
||||
"test:imports": "node --test test/esm.test.js test/i18n.test.js test/ssr.test.js",
|
||||
"typecheck": "node scripts/typecheck.mjs",
|
||||
"test:unit": "node --test test/playback.test.js test/dash-control.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js",
|
||||
"test:unit": "node --test test/playback.test.js test/dash-control.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js",
|
||||
"test": "yarn test:node && yarn test:baseline",
|
||||
"test:browser": "playwright test",
|
||||
"test:browser:install": "playwright install chromium firefox webkit",
|
||||
|
||||
@@ -4,6 +4,181 @@ 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.
|
||||
|
||||
## Display modes (CORE-16, in progress)
|
||||
|
||||
player/autoSizeMix.ts, autoHeightMix.ts, aspectRatioMix.ts and flipMix.ts retain the
|
||||
own-property facades, detached method binding, synchronous returns and normal events.
|
||||
display/sizing.ts contains geometry only; sizing-types.ts describes the minimal hosts.
|
||||
Zero/unready/non-finite media or container dimensions defer auto sizing without layout
|
||||
writes or invalid height events; the next valid call measures and applies normally.
|
||||
Aspect ratios retain split/Number coercion, datasets, notices and repeated events.
|
||||
Malformed or unusable ratios do not partially overwrite existing video geometry.
|
||||
Flip keeps custom strings and falsy normalization. Closing instances ignore these writes.
|
||||
Run `node --test test/display-sizing.test.js` and
|
||||
`yarn test:browser test/browser/display-sizing.spec.js` for geometry invariants, old/new
|
||||
property contracts, hidden-container recovery, notices and destruction. Composition with
|
||||
screen rotation is tested below; final installed-artifact acceptance remains a CORE-16 gate.
|
||||
|
||||
plugins/autoOrientation.ts composes the mobile-only builtin, retaining its name/state
|
||||
getter and mismatch rule. It rejects unready geometry and scopes the two ArtPlayer event
|
||||
subscriptions. orientation-types.ts models the minimal host and optional platform lock.
|
||||
The builtin registry currently bridges its generic JS host at this one assembly boundary;
|
||||
the complete constructor host remains for CORE-21, without widening the implementation.
|
||||
|
||||
display/orientation-web.ts owns delayed rotation and its four inline style properties.
|
||||
Each new fullscreen session cancels the previous timer. Repeated entry reapplies dimensions
|
||||
without replacing the first snapshot. On ordinary exit fullscreenWeb has already restored
|
||||
the full entry style, so rotation only clears its flags; destroy restores rotation properties
|
||||
before fullscreenWeb performs its own final restoration. Resize may update control CSS
|
||||
variables independently. The scope cancels timers and removes subscriptions on destruction.
|
||||
|
||||
display/orientation-native.ts owns lock requests, error notices and fullscreen rotation
|
||||
classes. It calls lock synchronously, marks success only while current, and cancels on exit
|
||||
or destroy. A weak per-platform-object token prevents cancelled or older instances from
|
||||
unlocking a newer ArtPlayer request. Late cancelled success is released; obsolete failures
|
||||
cannot overwrite current notices. Unlock is best effort, consistent with the prior API;
|
||||
arbitrary application calls outside ArtPlayer cannot be tracked as owned locks.
|
||||
|
||||
Run `node --test test/display-orientation.test.js` and
|
||||
`yarn test:browser test/browser/display-orientation.spec.js` for cancellation, retry, shared
|
||||
screen ownership, repeated entry, styles, timers, real rotated playback and ratio/flip
|
||||
composition. The browser fixture uses a mobile UA with desktop engines and controlled
|
||||
orientation.lock; it does not establish physical screen rotation or safe-area device support.
|
||||
The [Screen Orientation specification](https://www.w3.org/TR/screen-orientation/) permits
|
||||
platform preconditions for lock and defines superseding requests; capability and device
|
||||
acceptance must stay distinct from controlled request-state tests.
|
||||
|
||||
player/fullscreenWebMix.ts is the boolean property facade. display/web-fullscreen.ts
|
||||
owns a single entry-session snapshot, reentry generations and destroy restoration;
|
||||
display/placement.ts captures the original parent and next sibling. Repeated entry
|
||||
preserves the first snapshot while keeping the existing true events. Exit restores
|
||||
the captured position even if FULLSCREEN_WEB_IN_BODY changes while active. Destruction
|
||||
restores the player before template.destroy, so removeHtml still owns the same tree.
|
||||
Failed exit retains the snapshot for retry; reentrant event/DOM callbacks supersede
|
||||
the earlier setter. Normal fullscreenWeb/resize ordering remains synchronous.
|
||||
|
||||
Destroy is terminal: if restoring the original position throws before reattachment,
|
||||
detach the displaced player so template cleanup cannot leave an orphan in body. The
|
||||
original error still propagates; if detachment also fails, the lifecycle collector
|
||||
retains both failures. A node already returned to its owner is left for normal
|
||||
removeHtml handling. This exceptional fallback can leave retained HTML detached.
|
||||
|
||||
Restore the exact style attribute before the exit event; downstream resize handlers
|
||||
may update their own CSS variables. display-web.spec.js checks that distinction along
|
||||
with original placement, repeated entry, failure, reentry and destruction. The native
|
||||
fullscreen/PiP/mini/sizing/rotation paths have migrated source, while complete
|
||||
acceptance remain CORE-16; this section does not certify physical mobile devices.
|
||||
|
||||
player/fullscreenMix.ts still installs the fullscreen descriptor once after metadata.
|
||||
display/fullscreen-adapter.ts reuses the unchanged screenfull vendor method mapping;
|
||||
display/native-fullscreen.ts owns instance state, native events, cancellation and notices.
|
||||
The getter and exit operation concern this player's container/video, not any document
|
||||
fullscreen element. Normal fullscreen events precede class changes and resize. Reentrant
|
||||
setters invalidate stale work while same-state reentry still updates the interface.
|
||||
|
||||
display/fullscreen-request.ts invokes the native API on the caller stack, preserving
|
||||
transient user activation. Native Promise results remain authoritative for their errors;
|
||||
void prefixed APIs settle from change/error events. Void exit waits until neither the
|
||||
instance player nor video owns fullscreen, even
|
||||
when the request target was the player. Another element becoming fullscreen completes
|
||||
the old exit without giving this instance ownership of that new element. Cancelled calls
|
||||
settle promptly and lose their instance listeners. Descriptor callers can observe rejection identity;
|
||||
ordinary assignments report a notice without creating an unhandled rejection.
|
||||
|
||||
Unabortable void entry has no Promise to observe after destruction. Only cancellation
|
||||
of that path creates display/fullscreen-abandoned.ts's document-lifetime change guard.
|
||||
It keeps weak element keys and native exit operations, never player instances/scopes;
|
||||
unrelated targets are ignored and a new entry clears the old cancellation. This shared
|
||||
guard intentionally outlives instances, unlike their ordinary change/error listeners.
|
||||
Post-destroy native exit is best effort: browser denial cannot be undone synchronously.
|
||||
|
||||
Run `node --test test/display-native.test.js` for controlled async/reentry semantics.
|
||||
For native gesture, recovery and subtitle checks:
|
||||
|
||||
```sh
|
||||
yarn test:browser test/browser/display-native.spec.js test/browser/display-web.spec.js test/browser/subtitle-lifecycle.spec.js
|
||||
```
|
||||
|
||||
Void cancellation in desktop browsers uses controlled native methods; it does not certify
|
||||
old browser versions or iOS video-only fullscreen. Remaining failure boundaries and
|
||||
final installed distribution acceptance remain in CORE-16.
|
||||
|
||||
display/video-fullscreen.ts owns the video-only WebKit fallback. It retains the
|
||||
synchronous property setter and native thrown error, and listens to video begin/end,
|
||||
presentation-mode changes and the existing document event bridge. The state helper in
|
||||
display/video-fullscreen-state.ts distinguishes fullscreen from PiP when a presentation
|
||||
mode exists, otherwise reads webkitDisplayingFullscreen or the video event fallback.
|
||||
Repeated signals emit one transition; an exit failure retains actual state for retry.
|
||||
|
||||
Destroy releases all instance listeners and exits active video presentation. A pending
|
||||
unabortable entry leaves only a static listener on that video with a weak cancellation
|
||||
mark; this callback references no ArtPlayer/scope and exits late entry. New entry removes
|
||||
the cancellation. Synchronous exit failure during destroy propagates through the normal
|
||||
lifecycle cleanup collector; late cancellation cleanup is best effort. No document-wide
|
||||
listener is installed for this video-only path.
|
||||
|
||||
Use `node --test test/display-video-fullscreen.test.js` and
|
||||
`yarn test:browser test/browser/display-video-fullscreen.spec.js` for controlled fallback
|
||||
checks, plus native/fullscreenWeb/subtitle tests for composition. The desktop fixture
|
||||
forces the fallback and emulates native video events; it does not prove physical iOS
|
||||
presentation or gesture behavior. Apple documents the distinct video events in
|
||||
[Controlling Media with JavaScript](https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/ControllingMediaWithJavaScript/ControllingMediaWithJavaScript.html).
|
||||
|
||||
player/pipMix.ts chooses callable native, WebKit or unsupported capabilities and installs
|
||||
the original property descriptor. display/native-pip.ts keeps the element/null getter,
|
||||
void setter, native entry/leave events and failure notices. It never exits another
|
||||
video's PiP; revisions suppress obsolete notices, cancellation rejects late entry, and
|
||||
destroy disposes listeners before exiting owned presentation. Request invocation remains
|
||||
synchronous for transient activation. Async rejection is handled internally because the
|
||||
historical void setter cannot expose a Promise; native synchronous errors still throw.
|
||||
An old queued leave event is ignored while this video still owns the current PiP session;
|
||||
actual ownership loss continues to emit the existing false notification.
|
||||
|
||||
display/webkit-pip.ts keeps its boolean getter and initial inline setup. It observes both
|
||||
presentation-mode and PiP events, deduplicates native signals, and retains repeated
|
||||
synchronous setter notifications. Capability is checked at entry so later media readiness
|
||||
can allow retry. Cancelled pending entry leaves only a static video-held callback and weak
|
||||
mark, with no art/scope closure. Inline cleanup never exits a different fullscreen mode.
|
||||
Destruction's synchronous WebKit failure follows the normal lifecycle error collector;
|
||||
late cleanup is best effort. Platform UI remains the browser's responsibility.
|
||||
|
||||
The internal PipProperty reflects the branch-dependent getter. Historical public boolean
|
||||
declarations remain unchanged pending BASE-TYPE-10 / CORE-21; this is an explicit legacy
|
||||
declaration discrepancy, not a reason to convert native JS consumers to boolean.
|
||||
Run `node --test test/display-pip.test.js` and
|
||||
`yarn test:browser test/browser/display-pip.spec.js test/browser/display-pip-webkit.spec.js`.
|
||||
The actual native test records capabilities and, when supported, a native window, media
|
||||
progress, exit and fullscreenWeb/native-fullscreen transitions. WebKit event injection
|
||||
is separate from physical Safari/iOS validation. See the
|
||||
[PiP specification](https://w3c.github.io/picture-in-picture/) and
|
||||
[Apple's presentation-mode API](https://developer.apple.com/documentation/webkitjs/adding_picture_in_picture_to_your_safari_media_controls).
|
||||
|
||||
player/miniMix.ts keeps the boolean mini descriptor. display/mini.ts owns entry/exit
|
||||
generations and video placement; mini-view.ts owns popup creation, controls, playback
|
||||
subscriptions and node removal. mini-drag.ts owns document mouse subscriptions and drag
|
||||
state; mini-layout.ts calculates finite saved/default coordinates and viewport bounds.
|
||||
The first popup keeps video-first DOM order and its default display; reuse moves the video
|
||||
last and uses flex as before. Repeated true/false events and storage keys left/top remain.
|
||||
|
||||
Capture the original video parent and next sibling once per mini session. Exit restores
|
||||
that location, with the snapshot retained on failure for retry. Detach the snapshot before
|
||||
restoring DOM so a reentrant entry can capture its own placement. Serialize entry during
|
||||
view creation to avoid duplicate popups from custom icon getters. Failed creation disposes
|
||||
its partial view; hide/destroy during construction never leaves visible stale UI.
|
||||
|
||||
Created popups and playback/drag listeners belong to the instance scope; hide keeps a
|
||||
reusable hidden popup but cancels drag, and destroy removes owned nodes and restores the
|
||||
video. A caller-provided template.$mini stays caller-owned and regains its prior display
|
||||
on destroy. Drag uses client coordinates for fixed positioning, clamps its final position,
|
||||
and writes numeric left/top. Normal defaults retain a 50px inset; non-finite/offscreen
|
||||
storage resets, and CSS max-width/max-height keep the popup inside a smaller viewport.
|
||||
|
||||
Run `node --test test/display-mini.test.js` and
|
||||
`yarn test:browser test/browser/display-mini.spec.js test/browser/display-pip.spec.js`.
|
||||
Browser checks include video placement, playback buttons, actual drag, failure/reentry,
|
||||
caller ownership, narrow screens and transitions from mini to web fullscreen/native PiP.
|
||||
Final installed display-mode acceptance is still pending with CORE-16.
|
||||
|
||||
## Subtitles (CORE-15)
|
||||
|
||||
src/subtitle/index.ts keeps the Component prototype, bound update, public methods and
|
||||
|
||||
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 n=(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 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}()),r.exports);const o=t(n),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,i=Object.prototype.hasOwnProperty,a=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={}))i.call(e,r)&&s(t,r,e[r]);if(o)for(var r of o(e))a.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 i=t=>{try{s(r.next(t))}catch(e){o(e)}},a=t=>{try{s(r.throw(t))}catch(e){o(e)}},s=t=>t.done?n(t.value):Promise.resolve(t.value).then(i,a);s((r=r.apply(t,e)).next())});function u(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var p,h={exports:{}};var f=(p||(p=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 i(t,e,n),a(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),i(s,l,c),a(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),i(l,c,d),a(l,c,d),o(l,c,d)})}}(t,e,n),t}function i(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 a(t,e,n){if("function"===r(e)){var o=e(t,r(t),n);if(!0!==o){var i=r(o);throw"string"===i?new Error(o):"error"===i?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=u(f),g="5.4.1",v={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","meLine truncated
|
||||
+1200
-460
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,48 @@
|
||||
import type { NativeFullscreenAdapter } from './types'
|
||||
|
||||
const abandoned = new WeakMap<Document, WeakMap<Element, () => unknown>>()
|
||||
|
||||
export function hasAbandonedFullscreen(adapter: NativeFullscreenAdapter): boolean {
|
||||
return abandoned.get(adapter.document)?.has(adapter.target) ?? false
|
||||
}
|
||||
|
||||
export function clearAbandonedFullscreen(adapter: NativeFullscreenAdapter): void {
|
||||
abandoned.get(adapter.document)?.delete(adapter.target)
|
||||
}
|
||||
|
||||
function exitNative(exit: () => unknown): void {
|
||||
try {
|
||||
void Promise.resolve(exit()).catch(() => {})
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
|
||||
function entriesFor(document: Document, elementProperty: string, changeEvent: string): WeakMap<Element, () => unknown> {
|
||||
let entries = abandoned.get(document)
|
||||
if (!entries) {
|
||||
entries = new WeakMap()
|
||||
abandoned.set(document, entries)
|
||||
const pending = entries
|
||||
// A document-lifetime guard for unabortable void APIs. Keys are weak and values
|
||||
// contain only the native exit operation, never an ArtPlayer instance or scope.
|
||||
document.addEventListener(changeEvent, () => {
|
||||
const element = Reflect.get(document, elementProperty) as Element | null
|
||||
const release = element && pending.get(element)
|
||||
if (!release)
|
||||
return
|
||||
pending.delete(element)
|
||||
exitNative(release)
|
||||
})
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
export function abandonFullscreen(adapter: NativeFullscreenAdapter): void {
|
||||
const { document, elementProperty, changeEvent, target, exit } = adapter
|
||||
const entries = entriesFor(document, elementProperty, changeEvent)
|
||||
entries.set(target, exit)
|
||||
if (adapter.element === target) {
|
||||
entries.delete(target)
|
||||
exitNative(exit)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { NativeFullscreenAdapter } from './types'
|
||||
import screenfull from '../libs/screenfull'
|
||||
|
||||
interface MethodMap {
|
||||
requestFullscreen: string
|
||||
exitFullscreen: string
|
||||
fullscreenElement: string
|
||||
fullscreenEnabled: string
|
||||
fullscreenchange: string
|
||||
fullscreenerror: string
|
||||
}
|
||||
|
||||
function call(target: object, name: string, ...args: unknown[]): unknown {
|
||||
const method: unknown = Reflect.get(target, name)
|
||||
if (typeof method !== 'function')
|
||||
throw new TypeError(`Fullscreen method ${name} is not available`)
|
||||
return method.apply(target, args)
|
||||
}
|
||||
|
||||
export function fullscreenAdapter(target: HTMLElement): NativeFullscreenAdapter | undefined {
|
||||
// Reuse the audited vendor mapping, not its unscoped request-promise listeners.
|
||||
const raw = screenfull.raw as MethodMap | false
|
||||
const document = target.ownerDocument
|
||||
if (!raw || !Reflect.get(document, raw.fullscreenEnabled))
|
||||
return undefined
|
||||
return {
|
||||
document,
|
||||
target,
|
||||
get element(): Element | null {
|
||||
return Reflect.get(document, raw.fullscreenElement) as Element | null
|
||||
},
|
||||
elementProperty: raw.fullscreenElement,
|
||||
changeEvent: raw.fullscreenchange,
|
||||
errorEvent: raw.fullscreenerror,
|
||||
request: () => call(target, raw.requestFullscreen, undefined),
|
||||
exit: () => call(document, raw.exitFullscreen),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import type ResourceScope from '../lifecycle/scope'
|
||||
import type { NativeFullscreenAdapter } from './types'
|
||||
import { listen } from '../lifecycle/resources'
|
||||
import { abandonFullscreen, clearAbandonedFullscreen } from './fullscreen-abandoned'
|
||||
|
||||
export function requestFullscreen(adapter: NativeFullscreenAdapter, parent: ResourceScope, entering: boolean, lateCompletion: () => void, hasExited = () => adapter.element !== adapter.target): { promise: Promise<void>, cancel: () => void } {
|
||||
const scope = parent.child()
|
||||
let done = false
|
||||
let cancelled = false
|
||||
let returned = false
|
||||
let promiseResult = false
|
||||
let eventFailed = false
|
||||
let resolve!: () => void
|
||||
let reject!: (error: unknown) => void
|
||||
const promise = new Promise<void>((accept, fail) => {
|
||||
resolve = accept
|
||||
reject = fail
|
||||
})
|
||||
scope.add(() => {
|
||||
if (!done) {
|
||||
done = true
|
||||
cancelled = true
|
||||
resolve()
|
||||
if (returned && !promiseResult && entering)
|
||||
abandonFullscreen(adapter)
|
||||
}
|
||||
})
|
||||
function finish(failed = false, error?: unknown) {
|
||||
if (done)
|
||||
return
|
||||
done = true
|
||||
try {
|
||||
scope.dispose()
|
||||
}
|
||||
catch (cleanupError) {
|
||||
if (!failed) {
|
||||
failed = true
|
||||
error = cleanupError
|
||||
}
|
||||
}
|
||||
if (failed)
|
||||
reject(error)
|
||||
else
|
||||
resolve()
|
||||
}
|
||||
const changed = () => {
|
||||
if (returned && !promiseResult && (entering ? adapter.element === adapter.target : hasExited()))
|
||||
finish()
|
||||
}
|
||||
listen(scope, adapter.document, adapter.changeEvent, changed)
|
||||
listen(scope, adapter.document, adapter.errorEvent, (event) => {
|
||||
if (event.target === adapter.target || event.target === adapter.document) {
|
||||
eventFailed = true
|
||||
if (returned && !promiseResult)
|
||||
finish(true, new Error('Fullscreen request failed'))
|
||||
}
|
||||
})
|
||||
if (!scope.closed) {
|
||||
try {
|
||||
// Invoke before yielding so the browser sees the caller's transient activation.
|
||||
if (entering)
|
||||
clearAbandonedFullscreen(adapter)
|
||||
const result = entering ? adapter.request() : adapter.exit()
|
||||
promiseResult = Boolean(result && typeof (result as PromiseLike<unknown>).then === 'function')
|
||||
returned = true
|
||||
if (cancelled && !promiseResult && entering)
|
||||
abandonFullscreen(adapter)
|
||||
if (promiseResult) {
|
||||
void Promise.resolve(result).then(() => {
|
||||
if (cancelled)
|
||||
lateCompletion()
|
||||
else
|
||||
finish()
|
||||
}, error => finish(true, error)).catch(() => {})
|
||||
}
|
||||
else if (eventFailed) {
|
||||
finish(true, new Error('Fullscreen request failed'))
|
||||
}
|
||||
else {
|
||||
changed()
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
finish(true, error)
|
||||
}
|
||||
}
|
||||
return { promise, cancel: () => scope.dispose() }
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import type ResourceScope from '../lifecycle/scope'
|
||||
import type { MiniHost } from './types'
|
||||
import { listen } from '../lifecycle/resources'
|
||||
import { clampMini, miniGeometry } from './mini-layout'
|
||||
|
||||
export function miniDrag(art: MiniHost, element: HTMLElement, scope: ResourceScope, active: () => boolean): () => void {
|
||||
let dragging = false
|
||||
let startX = 0
|
||||
let startY = 0
|
||||
const cancel = (): undefined => {
|
||||
dragging = false
|
||||
element.classList.remove('art-mini-dragging')
|
||||
element.style.transform = ''
|
||||
}
|
||||
listen(scope, element, 'mousedown', (event) => {
|
||||
const mouse = event as MouseEvent
|
||||
if (mouse.button !== 0 || !active())
|
||||
return
|
||||
dragging = true
|
||||
startX = mouse.clientX
|
||||
startY = mouse.clientY
|
||||
})
|
||||
const move = (event: MouseEvent) => {
|
||||
if (!scope.closed && dragging && active()) {
|
||||
element.classList.add('art-mini-dragging')
|
||||
element.style.transform = `translate(${event.clientX - startX}px, ${event.clientY - startY}px)`
|
||||
}
|
||||
}
|
||||
const end = () => {
|
||||
if (scope.closed || !dragging || !active())
|
||||
return
|
||||
const rect = element.getBoundingClientRect()
|
||||
const position = clampMini(rect.left, rect.top, miniGeometry(element))
|
||||
cancel()
|
||||
element.style.left = `${position.left}px`
|
||||
element.style.top = `${position.top}px`
|
||||
art.storage.set('left', position.left)
|
||||
if (!scope.closed)
|
||||
art.storage.set('top', position.top)
|
||||
}
|
||||
art.on('document:mousemove', move)
|
||||
scope.add(() => {
|
||||
art.off('document:mousemove', move)
|
||||
})
|
||||
art.on('document:mouseup', end)
|
||||
scope.add(() => {
|
||||
art.off('document:mouseup', end)
|
||||
})
|
||||
scope.add(cancel)
|
||||
return cancel
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
export interface MiniGeometry {
|
||||
width: number
|
||||
height: number
|
||||
viewportWidth: number
|
||||
viewportHeight: number
|
||||
}
|
||||
|
||||
export function clampMini(left: number, top: number, geometry: MiniGeometry): { left: number, top: number } {
|
||||
const maxLeft = Math.max(0, geometry.viewportWidth - geometry.width)
|
||||
const maxTop = Math.max(0, geometry.viewportHeight - geometry.height)
|
||||
return { left: Math.max(0, Math.min(left, maxLeft)), top: Math.max(0, Math.min(top, maxTop)) }
|
||||
}
|
||||
|
||||
export function miniPosition(left: unknown, top: unknown, geometry: MiniGeometry): { left: number, top: number, reset: boolean } {
|
||||
if (typeof left === 'number' && Number.isFinite(left) && typeof top === 'number' && Number.isFinite(top)
|
||||
&& left >= 0 && top >= 0 && left + geometry.width <= geometry.viewportWidth && top + geometry.height <= geometry.viewportHeight) { return { left, top, reset: false } }
|
||||
return { ...clampMini(geometry.viewportWidth - geometry.width - 50, geometry.viewportHeight - geometry.height - 50, geometry), reset: true }
|
||||
}
|
||||
|
||||
export function miniGeometry(element: HTMLElement): MiniGeometry {
|
||||
const { width, height } = element.getBoundingClientRect()
|
||||
const view = element.ownerDocument.defaultView!
|
||||
return { width, height, viewportWidth: view.innerWidth, viewportHeight: view.innerHeight }
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import type ResourceScope from '../lifecycle/scope'
|
||||
import type { MiniHost } from './types'
|
||||
import { listen } from '../lifecycle/resources'
|
||||
import { silencePromise } from '../utils/error'
|
||||
import { miniDrag } from './mini-drag'
|
||||
|
||||
export interface MiniView {
|
||||
element: HTMLElement
|
||||
cancelDrag: () => void
|
||||
fresh: boolean
|
||||
}
|
||||
|
||||
export function createMiniView(art: MiniHost, parent: ResourceScope, active: () => boolean, hide: () => void): MiniView | undefined {
|
||||
const scope = parent.child()
|
||||
if (scope.closed)
|
||||
return
|
||||
const existing = art.template.$mini
|
||||
if (existing) {
|
||||
const display = existing.style.display
|
||||
scope.add(() => {
|
||||
existing.style.display = display
|
||||
})
|
||||
return { element: existing, cancelDrag: () => {}, fresh: false }
|
||||
}
|
||||
const document = art.template.$player.ownerDocument
|
||||
const element = document.createElement('div')
|
||||
element.className = 'art-mini-popup'
|
||||
scope.add(() => {
|
||||
element.remove()
|
||||
})
|
||||
try {
|
||||
const close = document.createElement('div')
|
||||
close.className = 'art-mini-close'
|
||||
close.append(art.icons.close)
|
||||
if (scope.closed)
|
||||
return
|
||||
const state = document.createElement('div')
|
||||
state.className = 'art-mini-state'
|
||||
const play = art.icons.play
|
||||
const pause = art.icons.pause
|
||||
if (scope.closed)
|
||||
return
|
||||
state.append(play, pause)
|
||||
element.append(close, state)
|
||||
listen(scope, close, 'click', hide)
|
||||
listen(scope, play, 'click', () => {
|
||||
silencePromise(art.play())
|
||||
})
|
||||
listen(scope, pause, 'click', () => {
|
||||
art.pause()
|
||||
})
|
||||
const update = () => {
|
||||
if (scope.closed)
|
||||
return
|
||||
play.style.display = art.playing ? 'none' : 'flex'
|
||||
pause.style.display = art.playing ? 'flex' : 'none'
|
||||
}
|
||||
update()
|
||||
for (const name of ['video:playing', 'video:pause', 'video:timeupdate'] as const) {
|
||||
art.on(name, update)
|
||||
scope.add(() => {
|
||||
art.off(name, update)
|
||||
})
|
||||
}
|
||||
const cancelDrag = miniDrag(art, element, scope, active)
|
||||
if (scope.closed)
|
||||
return
|
||||
art.template.$mini = element
|
||||
document.body.append(element)
|
||||
if (!scope.closed)
|
||||
return { element, cancelDrag, fresh: true }
|
||||
}
|
||||
catch (error) {
|
||||
scope.dispose()
|
||||
if (art.template.$mini === element)
|
||||
delete art.template.$mini
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import type { MiniView } from './mini-view'
|
||||
import type { Placement } from './placement'
|
||||
import type { MiniHost } from './types'
|
||||
import { getScope, isClosing } from '../lifecycle/instance'
|
||||
import { miniGeometry, miniPosition } from './mini-layout'
|
||||
import { createMiniView } from './mini-view'
|
||||
import { capturePlacement, restorePlacement } from './placement'
|
||||
|
||||
export function mini(art: MiniHost): PropertyDescriptor {
|
||||
const { $player, $video } = art.template
|
||||
const scope = getScope(art).child()
|
||||
let view: MiniView | undefined
|
||||
let placement: Placement | undefined
|
||||
let revision = 0
|
||||
let creating = false
|
||||
let queued = false
|
||||
const active = () => !isClosing(art) && $player.classList.contains('art-mini')
|
||||
|
||||
function restore() {
|
||||
if (placement) {
|
||||
const saved = placement
|
||||
const current = revision
|
||||
placement = undefined
|
||||
try {
|
||||
restorePlacement(saved)
|
||||
}
|
||||
catch (error) {
|
||||
if (current === revision && !placement)
|
||||
placement = saved
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hide() {
|
||||
if (isClosing(art))
|
||||
return
|
||||
const current = ++revision
|
||||
queued = false
|
||||
restore()
|
||||
if (isClosing(art) || current !== revision)
|
||||
return
|
||||
view?.cancelDrag()
|
||||
$player.classList.remove('art-mini')
|
||||
if (art.template.$mini) {
|
||||
art.template.$mini.style.display = 'none'
|
||||
art.emit('mini', false)
|
||||
}
|
||||
}
|
||||
|
||||
scope.add(() => {
|
||||
revision++
|
||||
try {
|
||||
restore()
|
||||
}
|
||||
finally {
|
||||
view?.cancelDrag()
|
||||
$player.classList.remove('art-mini')
|
||||
}
|
||||
})
|
||||
|
||||
function show(): void {
|
||||
if (isClosing(art))
|
||||
return
|
||||
const current = ++revision
|
||||
if (creating) {
|
||||
queued = true
|
||||
return
|
||||
}
|
||||
art.state = 'mini'
|
||||
if (isClosing(art) || current !== revision)
|
||||
return
|
||||
placement ??= capturePlacement($video)
|
||||
$player.classList.add('art-mini')
|
||||
try {
|
||||
if (!view) {
|
||||
creating = true
|
||||
try {
|
||||
view = createMiniView(art, scope, active, hide)
|
||||
}
|
||||
finally {
|
||||
creating = false
|
||||
}
|
||||
}
|
||||
if (queued) {
|
||||
queued = false
|
||||
show()
|
||||
return
|
||||
}
|
||||
if (!view || isClosing(art))
|
||||
return
|
||||
if (current !== revision) {
|
||||
if (!active())
|
||||
view.element.style.display = 'none'
|
||||
return
|
||||
}
|
||||
if (view.fresh)
|
||||
view.element.prepend($video)
|
||||
else
|
||||
view.element.append($video)
|
||||
if (isClosing(art) || current !== revision)
|
||||
return
|
||||
view.element.style.display = view.fresh ? '' : 'flex'
|
||||
view.fresh = false
|
||||
const top = art.storage.get('top')
|
||||
const left = art.storage.get('left')
|
||||
const position = miniPosition(left, top, miniGeometry(view.element))
|
||||
if (isClosing(art) || current !== revision)
|
||||
return
|
||||
view.element.style.left = `${position.left}px`
|
||||
view.element.style.top = `${position.top}px`
|
||||
if (position.reset) {
|
||||
art.storage.set('top', position.top)
|
||||
if (isClosing(art) || current !== revision)
|
||||
return
|
||||
art.storage.set('left', position.left)
|
||||
}
|
||||
if (!isClosing(art) && current === revision)
|
||||
art.emit('mini', true)
|
||||
}
|
||||
catch (error) {
|
||||
const pendingReentry = queued
|
||||
queued = false
|
||||
if (!isClosing(art) && (current === revision || pendingReentry)) {
|
||||
restore()
|
||||
$player.classList.remove('art-mini')
|
||||
if (view)
|
||||
view.element.style.display = 'none'
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
return {
|
||||
get: active,
|
||||
set(value: boolean): void {
|
||||
if (value)
|
||||
show()
|
||||
else
|
||||
hide()
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import type { FullscreenHost, NativeFullscreenAdapter } from './types'
|
||||
import { getScope, isClosing } from '../lifecycle/instance'
|
||||
import { listen } from '../lifecycle/resources'
|
||||
import { hasAbandonedFullscreen } from './fullscreen-abandoned'
|
||||
import { requestFullscreen } from './fullscreen-request'
|
||||
|
||||
export function nativeFullscreen(art: FullscreenHost, adapter: NativeFullscreenAdapter): PropertyDescriptor {
|
||||
const scope = getScope(art).child()
|
||||
const { $player, $video } = art.template
|
||||
const ownsFullscreen = () => adapter.element === $player || adapter.element === $video
|
||||
let last = ownsFullscreen()
|
||||
let revision = 0
|
||||
let desired = last
|
||||
let pending: ReturnType<typeof requestFullscreen> | undefined
|
||||
let pendingEntry = false
|
||||
let cancelledEntry = false
|
||||
let requesting = false
|
||||
|
||||
function exitOwned() {
|
||||
if (!ownsFullscreen())
|
||||
return
|
||||
try {
|
||||
void Promise.resolve(adapter.exit()).catch(() => {})
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
|
||||
scope.add(() => {
|
||||
revision += 1
|
||||
desired = false
|
||||
$player.classList.remove('art-fullscreen')
|
||||
exitOwned()
|
||||
})
|
||||
listen(scope, adapter.document, adapter.changeEvent, () => {
|
||||
const value = ownsFullscreen()
|
||||
if (value && hasAbandonedFullscreen(adapter))
|
||||
return
|
||||
if (value && cancelledEntry) {
|
||||
exitOwned()
|
||||
return
|
||||
}
|
||||
if (value === last || isClosing(art))
|
||||
return
|
||||
last = value
|
||||
const current = revision
|
||||
const stale = () => isClosing(art) || ownsFullscreen() !== value || (current !== revision && desired !== value)
|
||||
art.emit('fullscreen', value)
|
||||
if (stale())
|
||||
return
|
||||
if (value)
|
||||
art.state = 'fullscreen'
|
||||
if (stale())
|
||||
return
|
||||
$player.classList.toggle('art-fullscreen', value)
|
||||
art.emit('resize')
|
||||
})
|
||||
listen(scope, adapter.document, adapter.errorEvent, (event) => {
|
||||
const ownEvent = event.target === $player || event.target === $video
|
||||
|| (event.target === adapter.document && (requesting || pending !== undefined || ownsFullscreen()))
|
||||
if (ownEvent && !isClosing(art))
|
||||
art.emit('fullscreenError', event)
|
||||
})
|
||||
|
||||
return {
|
||||
get: () => !isClosing(art) && ownsFullscreen(),
|
||||
set(value: boolean): Promise<void> {
|
||||
if (isClosing(art))
|
||||
return Promise.resolve()
|
||||
const current = ++revision
|
||||
desired = Boolean(value)
|
||||
if (desired)
|
||||
cancelledEntry = false
|
||||
else if (pendingEntry)
|
||||
cancelledEntry = true
|
||||
pending?.cancel()
|
||||
pending = undefined
|
||||
pendingEntry = false
|
||||
if (!desired && !ownsFullscreen())
|
||||
return Promise.resolve()
|
||||
requesting = true
|
||||
pendingEntry = desired
|
||||
const operation = requestFullscreen(adapter, scope, desired, () => {
|
||||
if (isClosing(art) || !desired)
|
||||
exitOwned()
|
||||
}, () => !ownsFullscreen())
|
||||
requesting = false
|
||||
if (revision === current)
|
||||
pending = operation
|
||||
else
|
||||
operation.cancel()
|
||||
const result = operation.promise.catch((error) => {
|
||||
if (!isClosing(art) && revision === current)
|
||||
art.notice.show = error
|
||||
throw error
|
||||
}).finally(() => {
|
||||
if (revision === current) {
|
||||
pending = undefined
|
||||
pendingEntry = false
|
||||
}
|
||||
})
|
||||
// Assignment cannot consume a setter return, but descriptor callers still can.
|
||||
void result.catch(() => {})
|
||||
return result
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import type { PipHost } from './types'
|
||||
import { getScope, isClosing } from '../lifecycle/instance'
|
||||
import { listen } from '../lifecycle/resources'
|
||||
|
||||
export function nativePip(art: PipHost): PropertyDescriptor {
|
||||
const { $video } = art.template
|
||||
const document = $video.ownerDocument
|
||||
const scope = getScope(art).child()
|
||||
const owns = () => document.pictureInPictureElement === $video
|
||||
let last = owns()
|
||||
let desired = last
|
||||
let pending = false
|
||||
let cancelled = false
|
||||
let revision = 0
|
||||
$video.disablePictureInPicture = false
|
||||
|
||||
function exitOwned() {
|
||||
if (!owns())
|
||||
return
|
||||
try {
|
||||
void document.exitPictureInPicture().catch(() => {})
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
|
||||
function observe(result: Promise<unknown>, entering: boolean, current: number) {
|
||||
void result.then(() => {
|
||||
if (entering && (isClosing(art) || !desired))
|
||||
exitOwned()
|
||||
}, (error) => {
|
||||
if (!isClosing(art) && revision === current)
|
||||
art.notice.show = error
|
||||
}).finally(() => {
|
||||
if (revision === current)
|
||||
pending = false
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
scope.add(() => {
|
||||
revision++
|
||||
desired = false
|
||||
exitOwned()
|
||||
})
|
||||
listen(scope, $video, 'enterpictureinpicture', () => {
|
||||
if (cancelled) {
|
||||
exitOwned()
|
||||
return
|
||||
}
|
||||
if (!owns() || last)
|
||||
return
|
||||
const current = revision
|
||||
art.state = 'pip'
|
||||
if (!isClosing(art) && current === revision && owns()) {
|
||||
last = true
|
||||
art.emit('pip', true)
|
||||
}
|
||||
})
|
||||
listen(scope, $video, 'leavepictureinpicture', () => {
|
||||
if (!last || owns())
|
||||
return
|
||||
last = false
|
||||
art.emit('pip', false)
|
||||
})
|
||||
|
||||
return {
|
||||
// Native callers historically receive the media element, not a boolean.
|
||||
get: () => !isClosing(art) && owns() ? $video : null,
|
||||
set(value: boolean): void {
|
||||
if (isClosing(art))
|
||||
return
|
||||
const current = ++revision
|
||||
desired = Boolean(value)
|
||||
if (desired)
|
||||
cancelled = false
|
||||
else if (pending)
|
||||
cancelled = true
|
||||
pending = false
|
||||
if (desired) {
|
||||
art.state = 'pip'
|
||||
if (isClosing(art) || revision !== current)
|
||||
return
|
||||
pending = true
|
||||
try {
|
||||
// Native invocation stays on the user gesture stack; the setter stays void.
|
||||
observe($video.requestPictureInPicture!(), true, current)
|
||||
}
|
||||
catch (error) {
|
||||
if (revision === current)
|
||||
pending = false
|
||||
throw error
|
||||
}
|
||||
}
|
||||
else if (owns()) {
|
||||
observe(document.exitPictureInPicture(), false, current)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import type { OrientationHost, OrientationLock } from './orientation-types'
|
||||
import { getScope, isClosing } from '../lifecycle/instance'
|
||||
|
||||
interface Request {
|
||||
orientation: OrientationLock
|
||||
cancelled: boolean
|
||||
settled: boolean
|
||||
}
|
||||
|
||||
// Per-screen ownership prevents an old instance from unlocking a newer request.
|
||||
const owners = new WeakMap<OrientationLock, Request>()
|
||||
const className = 'art-auto-orientation-fullscreen'
|
||||
|
||||
function unlock(request: Request): void {
|
||||
if (owners.get(request.orientation) !== request)
|
||||
return
|
||||
try {
|
||||
request.orientation.unlock()
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
|
||||
export function nativeOrientation(art: OrientationHost, needRotate: () => boolean): (state: boolean) => void {
|
||||
const { $player } = art.template
|
||||
let current: Request | undefined
|
||||
|
||||
function cancel(): undefined {
|
||||
const request = current
|
||||
current = undefined
|
||||
$player.classList.remove(className)
|
||||
if (request) {
|
||||
request.cancelled = true
|
||||
unlock(request)
|
||||
if (request.settled && owners.get(request.orientation) === request)
|
||||
owners.delete(request.orientation)
|
||||
}
|
||||
}
|
||||
|
||||
getScope(art).add(cancel)
|
||||
return (state) => {
|
||||
if (isClosing(art))
|
||||
return
|
||||
if (!state) {
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
if (current && !current.cancelled && owners.get(current.orientation) === current)
|
||||
return
|
||||
const orientation = $player.ownerDocument.defaultView?.screen?.orientation as OrientationLock | undefined
|
||||
if (typeof orientation?.lock !== 'function' || typeof orientation.unlock !== 'function' || !needRotate())
|
||||
return
|
||||
const request: Request = { orientation, cancelled: false, settled: false }
|
||||
current = request
|
||||
owners.set(orientation, request)
|
||||
const active = () => !isClosing(art) && !request.cancelled && current === request && owners.get(orientation) === request
|
||||
const failed = (error: unknown) => {
|
||||
request.settled = true
|
||||
const show = active()
|
||||
if (owners.get(orientation) === request)
|
||||
owners.delete(orientation)
|
||||
if (current === request)
|
||||
current = undefined
|
||||
if (show) {
|
||||
$player.classList.remove(className)
|
||||
art.notice.show = error
|
||||
}
|
||||
}
|
||||
try {
|
||||
// Invoke before yielding; fullscreen entry already supplied the gesture.
|
||||
const result = orientation.lock(orientation.type.startsWith('portrait') ? 'landscape' : 'portrait')
|
||||
Promise.resolve(result).then(() => {
|
||||
request.settled = true
|
||||
if (active()) {
|
||||
$player.classList.add(className)
|
||||
}
|
||||
else {
|
||||
unlock(request)
|
||||
if (owners.get(orientation) === request)
|
||||
owners.delete(orientation)
|
||||
}
|
||||
}).catch(failed)
|
||||
}
|
||||
catch (error) {
|
||||
failed(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { SubscriptionHost } from '../component/resources'
|
||||
|
||||
export interface OrientationHost extends SubscriptionHost<{ fullscreenWeb: [boolean], fullscreen: [boolean] }> {
|
||||
constructor: { AUTO_ORIENTATION_TIME?: number }
|
||||
template: { $player: HTMLElement, $video: { readonly videoWidth: number, readonly videoHeight: number } }
|
||||
fullscreenWeb: boolean
|
||||
isRotate: boolean
|
||||
notice: { set show(value: unknown) }
|
||||
emit: (name: 'resize') => unknown
|
||||
}
|
||||
|
||||
export interface OrientationLock {
|
||||
readonly type: string
|
||||
lock: (mode: 'portrait' | 'landscape') => Promise<void>
|
||||
unlock: () => void
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { OrientationHost } from './orientation-types'
|
||||
import { getScope, isClosing } from '../lifecycle/instance'
|
||||
import { timeout } from '../lifecycle/resources'
|
||||
import { getSafeAreaInsets } from '../utils'
|
||||
|
||||
const className = 'art-auto-orientation'
|
||||
const properties = ['width', 'height', 'transform-origin', 'transform'] as const
|
||||
|
||||
export function webOrientation(art: OrientationHost, needRotate: () => boolean): (state: boolean) => void {
|
||||
const { $player } = art.template
|
||||
const scope = getScope(art)
|
||||
let cancel = () => {}
|
||||
let saved: { name: string, value: string, priority: string }[] | undefined
|
||||
|
||||
function clear(): undefined {
|
||||
cancel()
|
||||
cancel = () => {}
|
||||
const previous = saved
|
||||
saved = undefined
|
||||
// fullscreenWeb restores its complete entry style before emitting false.
|
||||
if (previous && art.fullscreenWeb) {
|
||||
for (const { name, value, priority } of previous)
|
||||
$player.style.setProperty(name, value, priority)
|
||||
}
|
||||
const wasRotated = $player.classList.contains(className)
|
||||
$player.classList.remove(className)
|
||||
if (wasRotated) {
|
||||
art.isRotate = false
|
||||
if (!isClosing(art))
|
||||
art.emit('resize')
|
||||
}
|
||||
}
|
||||
|
||||
scope.add(clear)
|
||||
function apply(emit: boolean): void {
|
||||
if (!art.fullscreenWeb || !needRotate())
|
||||
return
|
||||
const insets = getSafeAreaInsets()
|
||||
if (isClosing(art) || !art.fullscreenWeb)
|
||||
return
|
||||
const viewport = $player.ownerDocument.documentElement
|
||||
saved ??= properties.map(name => ({ name, value: $player.style.getPropertyValue(name), priority: $player.style.getPropertyPriority(name) }))
|
||||
$player.style.width = `${viewport.clientHeight - insets.top - insets.bottom}px`
|
||||
$player.style.height = `${viewport.clientWidth - insets.left - insets.right}px`
|
||||
$player.style.transformOrigin = '0 0'
|
||||
$player.style.transform = `rotate(90deg) translate(${insets.top}px, -${viewport.clientWidth - insets.right}px)`
|
||||
$player.classList.add(className)
|
||||
art.isRotate = true
|
||||
if (emit)
|
||||
art.emit('resize')
|
||||
}
|
||||
|
||||
return (state) => {
|
||||
if (isClosing(art))
|
||||
return
|
||||
if (!state) {
|
||||
clear()
|
||||
return
|
||||
}
|
||||
if (saved) {
|
||||
apply(false)
|
||||
return
|
||||
}
|
||||
if (!needRotate())
|
||||
return
|
||||
cancel()
|
||||
cancel = timeout(scope, () => {
|
||||
if (!saved)
|
||||
apply(true)
|
||||
}, Number(art.constructor.AUTO_ORIENTATION_TIME ?? 0))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export interface Placement {
|
||||
node: Node
|
||||
parent: ParentNode | null
|
||||
next: ChildNode | null
|
||||
}
|
||||
|
||||
export function capturePlacement(node: Node): Placement {
|
||||
return { node, parent: node.parentNode, next: node.nextSibling }
|
||||
}
|
||||
|
||||
export function restorePlacement({ node, parent, next }: Placement): void {
|
||||
if (!parent) {
|
||||
node.parentNode?.removeChild(node)
|
||||
return
|
||||
}
|
||||
const anchor = next?.parentNode === parent ? next : null
|
||||
if (node.parentNode !== parent || node.nextSibling !== anchor)
|
||||
parent.insertBefore(node, anchor)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { PlaybackNotice } from '../media/playback'
|
||||
import type { Size } from './sizing'
|
||||
|
||||
interface MediaDimensions {
|
||||
readonly videoWidth: number
|
||||
readonly videoHeight: number
|
||||
}
|
||||
|
||||
export interface AutoSizeHost {
|
||||
template: { $container: HTMLElement, $player: HTMLElement, $video: MediaDimensions }
|
||||
readonly width: number
|
||||
readonly height: number
|
||||
emit: (name: 'autoSize', size: Size) => unknown
|
||||
}
|
||||
|
||||
export interface AutoHeightHost {
|
||||
template: { $container: HTMLElement, $video: MediaDimensions }
|
||||
emit: (name: 'autoHeight', height: number) => unknown
|
||||
}
|
||||
|
||||
export interface AspectRatioHost extends PlaybackNotice {
|
||||
template: { $player: HTMLElement, $video: HTMLElement }
|
||||
emit: (name: 'aspectRatio', ratio: string) => unknown
|
||||
}
|
||||
|
||||
export interface FlipHost extends PlaybackNotice {
|
||||
template: { $player: HTMLElement }
|
||||
emit: (name: 'flip', flip: string) => unknown
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
export interface Size {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export function positiveSize({ width, height }: Size): boolean {
|
||||
return Number.isFinite(width) && width > 0 && Number.isFinite(height) && height > 0
|
||||
}
|
||||
|
||||
export function containSize(container: Size, ratio: number): (Size & { axis: 'width' | 'height' }) | undefined {
|
||||
if (!positiveSize(container) || !Number.isFinite(ratio) || ratio <= 0)
|
||||
return
|
||||
const result = container.width / container.height > ratio
|
||||
? { width: ratio * container.height, height: container.height, axis: 'width' as const }
|
||||
: { width: container.width, height: container.width / ratio, axis: 'height' as const }
|
||||
return positiveSize(result) ? result : undefined
|
||||
}
|
||||
|
||||
export function proportionalHeight(width: number, media: Size): number | undefined {
|
||||
if (!Number.isFinite(width) || width <= 0 || !positiveSize(media))
|
||||
return
|
||||
const height = media.height * (width / media.width)
|
||||
return Number.isFinite(height) && height > 0 ? height : undefined
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import type { SubscriptionHost } from '../component/resources'
|
||||
import type { OptionalMediaCapabilities } from '../media/types'
|
||||
|
||||
export interface WebFullscreenHost {
|
||||
constructor: { FULLSCREEN_WEB_IN_BODY: boolean }
|
||||
template: { $container: HTMLElement, $player: HTMLElement }
|
||||
state: string
|
||||
emit: (name: 'fullscreenWeb' | 'resize', ...args: unknown[]) => unknown
|
||||
}
|
||||
|
||||
export interface WebFullscreenProperty {
|
||||
fullscreenWeb: boolean
|
||||
}
|
||||
|
||||
export interface FullscreenHost {
|
||||
template: { $player: HTMLElement, $video: HTMLElement & OptionalMediaCapabilities }
|
||||
state: string
|
||||
notice: { set show(value: unknown) }
|
||||
i18n: { get: (key: string) => string }
|
||||
emit: (name: 'fullscreen' | 'fullscreenError' | 'resize', ...args: unknown[]) => unknown
|
||||
once: (name: 'video:loadedmetadata', listener: () => void) => unknown
|
||||
on: (name: 'document:webkitfullscreenchange', listener: () => void) => unknown
|
||||
off: (name: 'video:loadedmetadata' | 'document:webkitfullscreenchange', listener: () => void) => unknown
|
||||
}
|
||||
|
||||
export interface NativeFullscreenAdapter {
|
||||
document: Document
|
||||
target: HTMLElement
|
||||
readonly element: Element | null
|
||||
elementProperty: string
|
||||
changeEvent: string
|
||||
errorEvent: string
|
||||
request: () => unknown
|
||||
exit: () => unknown
|
||||
}
|
||||
|
||||
export interface PipHost {
|
||||
template: { $video: HTMLElement & OptionalMediaCapabilities & { disablePictureInPicture?: boolean } }
|
||||
state: string
|
||||
notice: { set show(value: unknown) }
|
||||
i18n: { get: (key: string) => string }
|
||||
emit: (name: 'pip', value: boolean) => unknown
|
||||
}
|
||||
|
||||
export interface PipProperty {
|
||||
get pip(): HTMLElement | null | boolean
|
||||
set pip(value: boolean)
|
||||
}
|
||||
|
||||
export interface MiniEvents {
|
||||
'video:playing': []
|
||||
'video:pause': []
|
||||
'video:timeupdate': []
|
||||
'document:mousemove': [MouseEvent]
|
||||
'document:mouseup': [MouseEvent]
|
||||
}
|
||||
|
||||
export interface MiniHost extends SubscriptionHost<MiniEvents> {
|
||||
template: { $player: HTMLElement, $video: HTMLElement, $mini?: HTMLElement }
|
||||
icons: { close: HTMLElement, play: HTMLElement, pause: HTMLElement }
|
||||
storage: { get: (key: string) => unknown, set: (key: string, value: number) => unknown }
|
||||
state: string
|
||||
playing: boolean
|
||||
play: () => unknown
|
||||
pause: () => unknown
|
||||
emit: (name: 'mini', value: boolean) => unknown
|
||||
}
|
||||
|
||||
export interface MiniProperty {
|
||||
mini: boolean
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { OptionalMediaCapabilities } from '../media/types'
|
||||
|
||||
type Video = HTMLElement & OptionalMediaCapabilities
|
||||
|
||||
const abandoned = new WeakSet<Video>()
|
||||
|
||||
export function videoIsFullscreen(video: Video, observed = false): boolean {
|
||||
if (typeof video.webkitPresentationMode === 'string')
|
||||
return video.webkitPresentationMode === 'fullscreen'
|
||||
if (typeof video.webkitDisplayingFullscreen === 'boolean')
|
||||
return video.webkitDisplayingFullscreen
|
||||
return video.ownerDocument.fullscreenElement === video || observed
|
||||
}
|
||||
|
||||
export function clearAbandonedVideo(video: Video): void {
|
||||
abandoned.delete(video)
|
||||
video.removeEventListener('webkitbeginfullscreen', releaseAbandonedVideo)
|
||||
video.removeEventListener('webkitpresentationmodechanged', releaseAbandonedVideo)
|
||||
}
|
||||
|
||||
function releaseAbandonedVideo(this: Video, event: Event): void {
|
||||
if (!abandoned.has(this) || !videoIsFullscreen(this, event.type === 'webkitbeginfullscreen'))
|
||||
return
|
||||
clearAbandonedVideo(this)
|
||||
try {
|
||||
this.webkitExitFullscreen?.()
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
|
||||
export function abandonVideo(video: Video): void {
|
||||
abandoned.add(video)
|
||||
// The listener is held by the video itself, with no instance or scope closure.
|
||||
video.addEventListener('webkitbeginfullscreen', releaseAbandonedVideo)
|
||||
video.addEventListener('webkitpresentationmodechanged', releaseAbandonedVideo)
|
||||
}
|
||||
|
||||
export function isAbandonedVideo(video: Video): boolean {
|
||||
return abandoned.has(video)
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import type { FullscreenHost } from './types'
|
||||
import { getScope, isClosing } from '../lifecycle/instance'
|
||||
import { listen } from '../lifecycle/resources'
|
||||
import { abandonVideo, clearAbandonedVideo, isAbandonedVideo, videoIsFullscreen } from './video-fullscreen-state'
|
||||
|
||||
export function videoFullscreen(art: FullscreenHost): PropertyDescriptor {
|
||||
const { $video } = art.template
|
||||
const scope = getScope(art).child()
|
||||
let observed = false
|
||||
let last = videoIsFullscreen($video)
|
||||
let desired = last
|
||||
let pending = false
|
||||
let revision = 0
|
||||
|
||||
const active = () => videoIsFullscreen($video, observed)
|
||||
function changed(hint?: boolean) {
|
||||
if (isClosing(art) || isAbandonedVideo($video))
|
||||
return
|
||||
if (typeof hint === 'boolean')
|
||||
observed = hint
|
||||
const value = active()
|
||||
if (value)
|
||||
pending = false
|
||||
if (value === last)
|
||||
return
|
||||
last = value
|
||||
const current = revision
|
||||
const stale = () => isClosing(art) || active() !== value || (current !== revision && desired !== value)
|
||||
art.emit('fullscreen', value)
|
||||
if (stale())
|
||||
return
|
||||
if (value)
|
||||
art.state = 'fullscreen'
|
||||
if (!stale())
|
||||
art.emit('resize')
|
||||
}
|
||||
|
||||
scope.add(() => {
|
||||
revision++
|
||||
desired = false
|
||||
if (pending)
|
||||
abandonVideo($video)
|
||||
if (active())
|
||||
$video.webkitExitFullscreen?.()
|
||||
})
|
||||
listen(scope, $video, 'webkitbeginfullscreen', () => changed(true))
|
||||
listen(scope, $video, 'webkitendfullscreen', () => changed(false))
|
||||
listen(scope, $video, 'webkitpresentationmodechanged', () => changed())
|
||||
const documentChanged = () => changed()
|
||||
art.on('document:webkitfullscreenchange', documentChanged)
|
||||
scope.add(() => {
|
||||
art.off('document:webkitfullscreenchange', documentChanged)
|
||||
})
|
||||
|
||||
return {
|
||||
get: () => !isClosing(art) && active(),
|
||||
set(value: boolean): void {
|
||||
if (isClosing(art))
|
||||
return
|
||||
const current = ++revision
|
||||
desired = Boolean(value)
|
||||
if (value) {
|
||||
clearAbandonedVideo($video)
|
||||
art.state = 'fullscreen'
|
||||
if (isClosing(art) || current !== revision)
|
||||
return
|
||||
pending = true
|
||||
try {
|
||||
$video.webkitEnterFullscreen!()
|
||||
}
|
||||
catch (error) {
|
||||
if (current === revision)
|
||||
pending = false
|
||||
throw error
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (pending) {
|
||||
pending = false
|
||||
abandonVideo($video)
|
||||
}
|
||||
if (active())
|
||||
$video.webkitExitFullscreen!()
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import type { Placement } from './placement'
|
||||
import type { WebFullscreenHost } from './types'
|
||||
import { getScope, isClosing } from '../lifecycle/instance'
|
||||
import { ResourceCleanupError } from '../lifecycle/scope'
|
||||
import { capturePlacement, restorePlacement } from './placement'
|
||||
|
||||
interface Snapshot {
|
||||
placement: Placement
|
||||
style: string | null
|
||||
}
|
||||
|
||||
export function webFullscreen(art: WebFullscreenHost): (value: boolean) => void {
|
||||
const { $player } = art.template
|
||||
let saved: Snapshot | undefined
|
||||
let revision = 0
|
||||
|
||||
function restore(snapshot: Snapshot, active: () => boolean): boolean {
|
||||
if (snapshot.style === null)
|
||||
$player.removeAttribute('style')
|
||||
else
|
||||
$player.setAttribute('style', snapshot.style)
|
||||
if (!active())
|
||||
return false
|
||||
$player.classList.remove('art-fullscreen-web')
|
||||
if (!active())
|
||||
return false
|
||||
restorePlacement(snapshot.placement)
|
||||
return active()
|
||||
}
|
||||
|
||||
getScope(art).add(() => {
|
||||
revision += 1
|
||||
const snapshot = saved
|
||||
saved = undefined
|
||||
if (snapshot) {
|
||||
try {
|
||||
restore(snapshot, () => true)
|
||||
}
|
||||
catch (error) {
|
||||
// Destruction cannot retry: detach only if restoration left our node outside its owner.
|
||||
try {
|
||||
if ($player.parentNode !== snapshot.placement.parent)
|
||||
$player.parentNode?.removeChild($player)
|
||||
}
|
||||
catch (detachError) {
|
||||
throw new ResourceCleanupError([error, detachError])
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return (value) => {
|
||||
if (isClosing(art))
|
||||
return
|
||||
const current = ++revision
|
||||
const active = () => current === revision && !isClosing(art)
|
||||
if (value) {
|
||||
if (!saved)
|
||||
saved = { placement: capturePlacement($player), style: $player.getAttribute('style') }
|
||||
const snapshot = saved
|
||||
try {
|
||||
if (art.constructor.FULLSCREEN_WEB_IN_BODY && $player.parentNode !== document.body)
|
||||
document.body.appendChild($player)
|
||||
if (!active())
|
||||
return
|
||||
art.state = 'fullscreenWeb'
|
||||
if (!active())
|
||||
return
|
||||
$player.style.width = '100%'
|
||||
$player.style.height = '100%'
|
||||
$player.classList.add('art-fullscreen-web')
|
||||
}
|
||||
catch (error) {
|
||||
if (active() && restore(snapshot, active))
|
||||
saved = undefined
|
||||
throw error
|
||||
}
|
||||
if (!active())
|
||||
return
|
||||
art.emit('fullscreenWeb', true)
|
||||
}
|
||||
else {
|
||||
if (saved) {
|
||||
if (!restore(saved, active))
|
||||
return
|
||||
saved = undefined
|
||||
}
|
||||
else {
|
||||
$player.classList.remove('art-fullscreen-web')
|
||||
}
|
||||
if (!active())
|
||||
return
|
||||
art.emit('fullscreenWeb', false)
|
||||
}
|
||||
if (active())
|
||||
art.emit('resize')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import type { PipHost } from './types'
|
||||
import { getScope, isClosing } from '../lifecycle/instance'
|
||||
import { listen } from '../lifecycle/resources'
|
||||
|
||||
type Video = PipHost['template']['$video']
|
||||
const abandoned = new WeakSet<Video>()
|
||||
const active = (video: Video) => video.webkitPresentationMode === 'picture-in-picture'
|
||||
|
||||
function clearAbandoned(video: Video) {
|
||||
abandoned.delete(video)
|
||||
video.removeEventListener('webkitpresentationmodechanged', releaseAbandoned)
|
||||
video.removeEventListener('enterpictureinpicture', releaseAbandoned)
|
||||
}
|
||||
|
||||
function releaseAbandoned(this: Video) {
|
||||
if (!abandoned.has(this) || !active(this))
|
||||
return
|
||||
clearAbandoned(this)
|
||||
try {
|
||||
this.webkitSetPresentationMode?.('inline')
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
|
||||
function abandon(video: Video) {
|
||||
abandoned.add(video)
|
||||
video.addEventListener('webkitpresentationmodechanged', releaseAbandoned)
|
||||
video.addEventListener('enterpictureinpicture', releaseAbandoned)
|
||||
}
|
||||
|
||||
export function webkitPip(art: PipHost): PropertyDescriptor {
|
||||
const { $video } = art.template
|
||||
const scope = getScope(art).child()
|
||||
let last = false
|
||||
let pending = false
|
||||
let revision = 0
|
||||
let emissions = 0
|
||||
|
||||
$video.webkitSetPresentationMode!('inline')
|
||||
|
||||
function changed(force = false) {
|
||||
if (isClosing(art) || abandoned.has($video))
|
||||
return
|
||||
const value = active($video)
|
||||
if (value)
|
||||
pending = false
|
||||
if (value === last && !force)
|
||||
return
|
||||
const current = revision
|
||||
if (value)
|
||||
art.state = 'pip'
|
||||
if (isClosing(art) || revision !== current || active($video) !== value)
|
||||
return
|
||||
last = value
|
||||
emissions++
|
||||
art.emit('pip', value)
|
||||
}
|
||||
|
||||
scope.add(() => {
|
||||
revision++
|
||||
if (pending)
|
||||
abandon($video)
|
||||
if (active($video))
|
||||
$video.webkitSetPresentationMode!('inline')
|
||||
})
|
||||
for (const name of ['webkitpresentationmodechanged', 'enterpictureinpicture', 'leavepictureinpicture'])
|
||||
listen(scope, $video, name, () => changed())
|
||||
|
||||
return {
|
||||
get: () => !isClosing(art) && active($video),
|
||||
set(value: boolean): void {
|
||||
if (isClosing(art))
|
||||
return
|
||||
const current = ++revision
|
||||
const before = emissions
|
||||
if (value) {
|
||||
if ($video.webkitSupportsPresentationMode?.('picture-in-picture') === false) {
|
||||
art.notice.show = art.i18n.get('PIP Not Supported')
|
||||
return
|
||||
}
|
||||
clearAbandoned($video)
|
||||
art.state = 'pip'
|
||||
if (isClosing(art) || revision !== current)
|
||||
return
|
||||
pending = true
|
||||
try {
|
||||
$video.webkitSetPresentationMode!('picture-in-picture')
|
||||
}
|
||||
catch (error) {
|
||||
if (current === revision)
|
||||
pending = false
|
||||
throw error
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (pending) {
|
||||
pending = false
|
||||
abandon($video)
|
||||
}
|
||||
if (active($video))
|
||||
$video.webkitSetPresentationMode!('inline')
|
||||
}
|
||||
if (!isClosing(art) && current === revision && before === emissions && active($video) === Boolean(value))
|
||||
changed(true)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { def, setStyle } from '../utils'
|
||||
|
||||
export default function aspectRatioMix(art) {
|
||||
const {
|
||||
i18n,
|
||||
notice,
|
||||
template: { $video, $player },
|
||||
} = art
|
||||
|
||||
def(art, 'aspectRatio', {
|
||||
get() {
|
||||
return $player.dataset.aspectRatio || 'default'
|
||||
},
|
||||
set(ratio) {
|
||||
if (!ratio)
|
||||
ratio = 'default'
|
||||
if (ratio === 'default') {
|
||||
setStyle($video, 'width', null)
|
||||
setStyle($video, 'height', null)
|
||||
setStyle($video, 'margin', null)
|
||||
delete $player.dataset.aspectRatio
|
||||
}
|
||||
else {
|
||||
const ratioArray = ratio.split(':').map(Number)
|
||||
const { clientWidth, clientHeight } = $player
|
||||
const playerRatio = clientWidth / clientHeight
|
||||
const setupRatio = ratioArray[0] / ratioArray[1]
|
||||
|
||||
if (playerRatio > setupRatio) {
|
||||
setStyle($video, 'width', `${setupRatio * clientHeight}px`)
|
||||
setStyle($video, 'height', '100%')
|
||||
setStyle($video, 'margin', '0 auto')
|
||||
}
|
||||
else {
|
||||
setStyle($video, 'width', '100%')
|
||||
setStyle($video, 'height', `${clientWidth / setupRatio}px`)
|
||||
setStyle($video, 'margin', 'auto 0')
|
||||
}
|
||||
|
||||
$player.dataset.aspectRatio = ratio
|
||||
}
|
||||
|
||||
notice.show = `${i18n.get('Aspect Ratio')}: ${ratio === 'default' ? i18n.get('Default') : ratio}`
|
||||
art.emit('aspectRatio', ratio)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { AspectRatioHost } from '../display/sizing-types'
|
||||
import { containSize } from '../display/sizing'
|
||||
import { isClosing } from '../lifecycle/instance'
|
||||
import { def } from '../utils'
|
||||
|
||||
export default function aspectRatioMix(art: AspectRatioHost): void {
|
||||
const { i18n, notice, template: { $video, $player } } = art
|
||||
def(art, 'aspectRatio', {
|
||||
get: () => $player.dataset.aspectRatio || 'default',
|
||||
set(ratio: string) {
|
||||
if (isClosing(art))
|
||||
return
|
||||
if (!ratio)
|
||||
ratio = 'default'
|
||||
if (ratio === 'default') {
|
||||
$video.style.width = ''
|
||||
$video.style.height = ''
|
||||
$video.style.margin = ''
|
||||
delete $player.dataset.aspectRatio
|
||||
}
|
||||
else {
|
||||
// Keep the legacy split/Number coercion and dataset even for invalid ratios.
|
||||
const parts = ratio.split(':').map(Number)
|
||||
const size = containSize({ width: $player.clientWidth, height: $player.clientHeight }, parts[0]! / parts[1]!)
|
||||
if (isClosing(art))
|
||||
return
|
||||
if (size?.axis === 'width') {
|
||||
$video.style.width = `${size.width}px`
|
||||
$video.style.height = '100%'
|
||||
$video.style.margin = '0 auto'
|
||||
}
|
||||
else if (size) {
|
||||
$video.style.width = '100%'
|
||||
$video.style.height = `${size.height}px`
|
||||
$video.style.margin = 'auto 0'
|
||||
}
|
||||
$player.dataset.aspectRatio = ratio
|
||||
}
|
||||
notice.show = `${i18n.get('Aspect Ratio')}: ${ratio === 'default' ? i18n.get('Default') : ratio}`
|
||||
if (!isClosing(art))
|
||||
art.emit('aspectRatio', ratio)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { def, setStyle } from '../utils'
|
||||
|
||||
export default function autoHeightMix(art) {
|
||||
const {
|
||||
template: { $container, $video },
|
||||
} = art
|
||||
|
||||
def(art, 'autoHeight', {
|
||||
value() {
|
||||
const { clientWidth } = $container
|
||||
const { videoHeight, videoWidth } = $video
|
||||
const height = videoHeight * (clientWidth / videoWidth)
|
||||
setStyle($container, 'height', `${height}px`)
|
||||
art.emit('autoHeight', height)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { AutoHeightHost } from '../display/sizing-types'
|
||||
import { proportionalHeight } from '../display/sizing'
|
||||
import { isClosing } from '../lifecycle/instance'
|
||||
import { def } from '../utils'
|
||||
|
||||
export default function autoHeightMix(art: AutoHeightHost): void {
|
||||
const { $container, $video } = art.template
|
||||
def(art, 'autoHeight', {
|
||||
value() {
|
||||
if (isClosing(art))
|
||||
return
|
||||
const height = proportionalHeight($container.clientWidth, { width: $video.videoWidth, height: $video.videoHeight })
|
||||
if (height === undefined || isClosing(art))
|
||||
return
|
||||
$container.style.height = `${height}px`
|
||||
art.emit('autoHeight', height)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { def, getRect, setStyle } from '../utils'
|
||||
|
||||
export default function autoSizeMix(art) {
|
||||
const { $container, $player, $video } = art.template
|
||||
|
||||
def(art, 'autoSize', {
|
||||
value() {
|
||||
const { videoWidth, videoHeight } = $video
|
||||
const { width, height } = getRect($container)
|
||||
const videoRatio = videoWidth / videoHeight
|
||||
const containerRatio = width / height
|
||||
if (containerRatio > videoRatio) {
|
||||
const percentage = ((height * videoRatio) / width) * 100
|
||||
setStyle($player, 'width', `${percentage}%`)
|
||||
setStyle($player, 'height', '100%')
|
||||
}
|
||||
else {
|
||||
const percentage = (width / videoRatio / height) * 100
|
||||
setStyle($player, 'width', '100%')
|
||||
setStyle($player, 'height', `${percentage}%`)
|
||||
}
|
||||
art.emit('autoSize', {
|
||||
width: art.width,
|
||||
height: art.height,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { AutoSizeHost } from '../display/sizing-types'
|
||||
import { containSize, positiveSize } from '../display/sizing'
|
||||
import { isClosing } from '../lifecycle/instance'
|
||||
import { def, getRect } from '../utils'
|
||||
|
||||
export default function autoSizeMix(art: AutoSizeHost): void {
|
||||
const { $container, $player, $video } = art.template
|
||||
def(art, 'autoSize', {
|
||||
value() {
|
||||
if (isClosing(art))
|
||||
return
|
||||
const media = { width: $video.videoWidth, height: $video.videoHeight }
|
||||
if (!positiveSize(media))
|
||||
return
|
||||
const container = getRect($container)
|
||||
const size = containSize(container, media.width / media.height)
|
||||
if (!size || isClosing(art))
|
||||
return
|
||||
if (size.axis === 'width') {
|
||||
$player.style.width = `${(size.width / container.width) * 100}%`
|
||||
$player.style.height = '100%'
|
||||
}
|
||||
else {
|
||||
$player.style.width = '100%'
|
||||
$player.style.height = `${(size.height / container.height) * 100}%`
|
||||
}
|
||||
art.emit('autoSize', { width: art.width, height: art.height })
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { capitalize, def } from '../utils'
|
||||
|
||||
export default function flipMix(art) {
|
||||
const {
|
||||
template: { $player },
|
||||
i18n,
|
||||
notice,
|
||||
} = art
|
||||
|
||||
def(art, 'flip', {
|
||||
get() {
|
||||
return $player.dataset.flip || 'normal'
|
||||
},
|
||||
set(flip) {
|
||||
if (!flip)
|
||||
flip = 'normal'
|
||||
if (flip === 'normal') {
|
||||
delete $player.dataset.flip
|
||||
}
|
||||
else {
|
||||
$player.dataset.flip = flip
|
||||
}
|
||||
|
||||
notice.show = `${i18n.get('Video Flip')}: ${i18n.get(capitalize(flip))}`
|
||||
art.emit('flip', flip)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { FlipHost } from '../display/sizing-types'
|
||||
import { isClosing } from '../lifecycle/instance'
|
||||
import { capitalize, def } from '../utils'
|
||||
|
||||
export default function flipMix(art: FlipHost): void {
|
||||
const { template: { $player }, i18n, notice } = art
|
||||
def(art, 'flip', {
|
||||
get: () => $player.dataset.flip || 'normal',
|
||||
set(flip: string) {
|
||||
if (isClosing(art))
|
||||
return
|
||||
if (!flip)
|
||||
flip = 'normal'
|
||||
if (flip === 'normal')
|
||||
delete $player.dataset.flip
|
||||
else
|
||||
$player.dataset.flip = flip
|
||||
notice.show = `${i18n.get('Video Flip')}: ${i18n.get(capitalize(flip))}`
|
||||
if (!isClosing(art))
|
||||
art.emit('flip', flip)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
import screenfull from '../libs/screenfull'
|
||||
import { addClass, def, get, removeClass } from '../utils'
|
||||
|
||||
export default function fullscreenMix(art) {
|
||||
const {
|
||||
i18n,
|
||||
notice,
|
||||
template: { $video, $player },
|
||||
} = art
|
||||
|
||||
const nativeScreenfull = (art) => {
|
||||
screenfull.on('change', () => {
|
||||
art.emit('fullscreen', screenfull.isFullscreen)
|
||||
|
||||
if (screenfull.isFullscreen) {
|
||||
art.state = 'fullscreen'
|
||||
addClass($player, 'art-fullscreen')
|
||||
}
|
||||
else {
|
||||
removeClass($player, 'art-fullscreen')
|
||||
}
|
||||
|
||||
art.emit('resize')
|
||||
})
|
||||
|
||||
screenfull.on('error', (event) => {
|
||||
art.emit('fullscreenError', event)
|
||||
})
|
||||
|
||||
def(art, 'fullscreen', {
|
||||
get() {
|
||||
return screenfull.isFullscreen
|
||||
},
|
||||
async set(value) {
|
||||
if (value) {
|
||||
await screenfull.request($player)
|
||||
}
|
||||
else {
|
||||
await screenfull.exit()
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const webkitScreenfull = (art) => {
|
||||
art.on('document:webkitfullscreenchange', () => {
|
||||
art.emit('fullscreen', art.fullscreen)
|
||||
art.emit('resize')
|
||||
})
|
||||
|
||||
def(art, 'fullscreen', {
|
||||
get() {
|
||||
return document.fullscreenElement === $video
|
||||
},
|
||||
set(value) {
|
||||
if (value) {
|
||||
art.state = 'fullscreen'
|
||||
$video.webkitEnterFullscreen()
|
||||
}
|
||||
else {
|
||||
$video.webkitExitFullscreen()
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
art.once('video:loadedmetadata', () => {
|
||||
if (screenfull.isEnabled) {
|
||||
nativeScreenfull(art)
|
||||
}
|
||||
else if ($video.webkitSupportsFullscreen) {
|
||||
webkitScreenfull(art)
|
||||
}
|
||||
else {
|
||||
def(art, 'fullscreen', {
|
||||
get() {
|
||||
return false
|
||||
},
|
||||
set() {
|
||||
notice.show = i18n.get('Fullscreen Not Supported')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Asynchronous setting
|
||||
def(art, 'fullscreen', get(art, 'fullscreen'))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { FullscreenHost } from '../display/types'
|
||||
import { fullscreenAdapter } from '../display/fullscreen-adapter'
|
||||
import { nativeFullscreen } from '../display/native-fullscreen'
|
||||
import { videoFullscreen } from '../display/video-fullscreen'
|
||||
import { getScope, isClosing } from '../lifecycle/instance'
|
||||
import { def, get } from '../utils'
|
||||
|
||||
export default function fullscreenMix(art: FullscreenHost): void {
|
||||
const { i18n, notice, template: { $video, $player } } = art
|
||||
const metadata = () => {
|
||||
if (isClosing(art))
|
||||
return
|
||||
const adapter = fullscreenAdapter($player)
|
||||
if (adapter) {
|
||||
def(art, 'fullscreen', nativeFullscreen(art, adapter))
|
||||
}
|
||||
else if ($video.webkitSupportsFullscreen && typeof $video.webkitEnterFullscreen === 'function' && typeof $video.webkitExitFullscreen === 'function') {
|
||||
def(art, 'fullscreen', videoFullscreen(art))
|
||||
}
|
||||
else {
|
||||
def(art, 'fullscreen', {
|
||||
get: () => false,
|
||||
set() {
|
||||
if (!isClosing(art))
|
||||
notice.show = i18n.get('Fullscreen Not Supported')
|
||||
},
|
||||
})
|
||||
}
|
||||
def(art, 'fullscreen', get(art, 'fullscreen')!)
|
||||
}
|
||||
art.once('video:loadedmetadata', metadata)
|
||||
getScope(art).add(() => {
|
||||
art.off('video:loadedmetadata', metadata)
|
||||
})
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { addClass, append, def, hasClass, removeClass, setStyle } from '../utils'
|
||||
|
||||
export default function fullscreenWebMix(art) {
|
||||
const {
|
||||
constructor,
|
||||
template: { $container, $player },
|
||||
} = art
|
||||
|
||||
let cssText = ''
|
||||
def(art, 'fullscreenWeb', {
|
||||
get() {
|
||||
return hasClass($player, 'art-fullscreen-web')
|
||||
},
|
||||
set(value) {
|
||||
if (value) {
|
||||
cssText = $player.style.cssText
|
||||
if (constructor.FULLSCREEN_WEB_IN_BODY) {
|
||||
append(document.body, $player)
|
||||
}
|
||||
art.state = 'fullscreenWeb'
|
||||
setStyle($player, 'width', '100%')
|
||||
setStyle($player, 'height', '100%')
|
||||
addClass($player, 'art-fullscreen-web')
|
||||
art.emit('fullscreenWeb', true)
|
||||
}
|
||||
else {
|
||||
if (constructor.FULLSCREEN_WEB_IN_BODY) {
|
||||
append($container, $player)
|
||||
}
|
||||
if (cssText) {
|
||||
$player.style.cssText = cssText
|
||||
cssText = ''
|
||||
}
|
||||
removeClass($player, 'art-fullscreen-web')
|
||||
art.emit('fullscreenWeb', false)
|
||||
}
|
||||
|
||||
art.emit('resize')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { WebFullscreenHost, WebFullscreenProperty } from '../display/types'
|
||||
import { webFullscreen } from '../display/web-fullscreen'
|
||||
import { def } from '../utils'
|
||||
|
||||
export default function fullscreenWebMix(art: WebFullscreenHost): asserts art is WebFullscreenHost & WebFullscreenProperty {
|
||||
const setFullscreen = webFullscreen(art)
|
||||
def(art, 'fullscreenWeb', {
|
||||
get(): boolean {
|
||||
return art.template.$player.classList.contains('art-fullscreen-web')
|
||||
},
|
||||
set: setFullscreen,
|
||||
})
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
import { addClass, append, createElement, def, getRect, hasClass, isInViewport, removeClass, setStyle, silencePromise } from '../utils'
|
||||
|
||||
export default function miniMix(art) {
|
||||
const {
|
||||
icons,
|
||||
proxy,
|
||||
storage,
|
||||
template: { $player, $video },
|
||||
} = art
|
||||
|
||||
let isDragging = false
|
||||
let lastPageX = 0
|
||||
let lastPageY = 0
|
||||
|
||||
function hideMini() {
|
||||
const { $mini } = art.template
|
||||
if ($mini) {
|
||||
removeClass($player, 'art-mini')
|
||||
setStyle($mini, 'display', 'none')
|
||||
$player.prepend($video)
|
||||
art.emit('mini', false)
|
||||
}
|
||||
}
|
||||
|
||||
function initState($play, $pause) {
|
||||
if (art.playing) {
|
||||
setStyle($play, 'display', `none`)
|
||||
setStyle($pause, 'display', `flex`)
|
||||
}
|
||||
else {
|
||||
setStyle($play, 'display', `flex`)
|
||||
setStyle($pause, 'display', `none`)
|
||||
}
|
||||
}
|
||||
|
||||
function createMini() {
|
||||
const { $mini } = art.template
|
||||
if ($mini) {
|
||||
append($mini, $video)
|
||||
return setStyle($mini, 'display', 'flex')
|
||||
}
|
||||
else {
|
||||
const $mini = createElement('div')
|
||||
addClass($mini, 'art-mini-popup')
|
||||
append(document.body, $mini)
|
||||
art.template.$mini = $mini
|
||||
append($mini, $video)
|
||||
|
||||
const $close = append($mini, `<div class="art-mini-close"></div>`)
|
||||
append($close, icons.close)
|
||||
proxy($close, 'click', hideMini)
|
||||
|
||||
const $state = append($mini, `<div class="art-mini-state"></div>`)
|
||||
const $play = append($state, icons.play)
|
||||
const $pause = append($state, icons.pause)
|
||||
proxy($play, 'click', () => silencePromise(art.play()))
|
||||
proxy($pause, 'click', () => art.pause())
|
||||
initState($play, $pause)
|
||||
art.on('video:playing', () => initState($play, $pause))
|
||||
art.on('video:pause', () => initState($play, $pause))
|
||||
art.on('video:timeupdate', () => initState($play, $pause))
|
||||
|
||||
proxy($mini, 'mousedown', (event) => {
|
||||
isDragging = event.button === 0
|
||||
lastPageX = event.pageX
|
||||
lastPageY = event.pageY
|
||||
})
|
||||
|
||||
art.on('document:mousemove', (event) => {
|
||||
if (isDragging) {
|
||||
addClass($mini, 'art-mini-dragging')
|
||||
const x = event.pageX - lastPageX
|
||||
const y = event.pageY - lastPageY
|
||||
setStyle($mini, 'transform', `translate(${x}px, ${y}px)`)
|
||||
}
|
||||
})
|
||||
|
||||
art.on('document:mouseup', () => {
|
||||
if (isDragging) {
|
||||
isDragging = false
|
||||
removeClass($mini, 'art-mini-dragging')
|
||||
const rect = getRect($mini)
|
||||
storage.set('left', rect.left)
|
||||
storage.set('top', rect.top)
|
||||
setStyle($mini, 'left', `${rect.left}px`)
|
||||
setStyle($mini, 'top', `${rect.top}px`)
|
||||
setStyle($mini, 'transform', null)
|
||||
}
|
||||
})
|
||||
|
||||
return $mini
|
||||
}
|
||||
}
|
||||
|
||||
function initMini() {
|
||||
const { $mini } = art.template
|
||||
const rect = getRect($mini)
|
||||
const top = window.innerHeight - rect.height - 50
|
||||
const left = window.innerWidth - rect.width - 50
|
||||
storage.set('top', top)
|
||||
storage.set('left', left)
|
||||
setStyle($mini, 'top', `${top}px`)
|
||||
setStyle($mini, 'left', `${left}px`)
|
||||
}
|
||||
|
||||
def(art, 'mini', {
|
||||
get() {
|
||||
return hasClass($player, 'art-mini')
|
||||
},
|
||||
set(value) {
|
||||
if (value) {
|
||||
art.state = 'mini'
|
||||
addClass($player, 'art-mini')
|
||||
const $mini = createMini()
|
||||
const top = storage.get('top')
|
||||
const left = storage.get('left')
|
||||
if (typeof top === 'number' && typeof left === 'number') {
|
||||
setStyle($mini, 'top', `${top}px`)
|
||||
setStyle($mini, 'left', `${left}px`)
|
||||
if (!isInViewport($mini)) {
|
||||
initMini()
|
||||
}
|
||||
}
|
||||
else {
|
||||
initMini()
|
||||
}
|
||||
art.emit('mini', true)
|
||||
}
|
||||
else {
|
||||
hideMini()
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { MiniHost, MiniProperty } from '../display/types'
|
||||
import { mini } from '../display/mini'
|
||||
import { def } from '../utils'
|
||||
|
||||
export default function miniMix(art: MiniHost): asserts art is MiniHost & MiniProperty {
|
||||
def(art, 'mini', mini(art))
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
import { def } from '../utils'
|
||||
|
||||
function nativePip(art) {
|
||||
const {
|
||||
template: { $video },
|
||||
proxy,
|
||||
notice,
|
||||
} = art
|
||||
|
||||
$video.disablePictureInPicture = false
|
||||
|
||||
def(art, 'pip', {
|
||||
get() {
|
||||
return document.pictureInPictureElement
|
||||
},
|
||||
set(value) {
|
||||
if (value) {
|
||||
art.state = 'pip'
|
||||
$video.requestPictureInPicture().catch((err) => {
|
||||
notice.show = err
|
||||
throw err
|
||||
})
|
||||
}
|
||||
else {
|
||||
document.exitPictureInPicture().catch((err) => {
|
||||
notice.show = err
|
||||
throw err
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
proxy($video, 'enterpictureinpicture', () => {
|
||||
art.emit('pip', true)
|
||||
})
|
||||
|
||||
proxy($video, 'leavepictureinpicture', () => {
|
||||
art.emit('pip', false)
|
||||
})
|
||||
}
|
||||
|
||||
function webkitPip(art) {
|
||||
const { $video } = art.template
|
||||
|
||||
$video.webkitSetPresentationMode('inline')
|
||||
|
||||
def(art, 'pip', {
|
||||
get() {
|
||||
return $video.webkitPresentationMode === 'picture-in-picture'
|
||||
},
|
||||
set(value) {
|
||||
if (value) {
|
||||
art.state = 'pip'
|
||||
$video.webkitSetPresentationMode('picture-in-picture')
|
||||
art.emit('pip', true)
|
||||
}
|
||||
else {
|
||||
$video.webkitSetPresentationMode('inline')
|
||||
art.emit('pip', false)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export default function pipMix(art) {
|
||||
const {
|
||||
i18n,
|
||||
notice,
|
||||
template: { $video },
|
||||
} = art
|
||||
if (document.pictureInPictureEnabled) {
|
||||
nativePip(art)
|
||||
}
|
||||
else if ($video.webkitSupportsPresentationMode) {
|
||||
webkitPip(art)
|
||||
}
|
||||
else {
|
||||
def(art, 'pip', {
|
||||
get() {
|
||||
return false
|
||||
},
|
||||
set() {
|
||||
notice.show = i18n.get('PIP Not Supported')
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { PipHost, PipProperty } from '../display/types'
|
||||
import { nativePip } from '../display/native-pip'
|
||||
import { webkitPip } from '../display/webkit-pip'
|
||||
import { isClosing } from '../lifecycle/instance'
|
||||
import { def } from '../utils'
|
||||
|
||||
export default function pipMix(art: PipHost): asserts art is PipHost & PipProperty {
|
||||
const { i18n, notice, template: { $video } } = art
|
||||
const document = $video.ownerDocument
|
||||
if (document.pictureInPictureEnabled && typeof $video.requestPictureInPicture === 'function' && typeof document.exitPictureInPicture === 'function') {
|
||||
def(art, 'pip', nativePip(art))
|
||||
}
|
||||
else if (typeof $video.webkitSupportsPresentationMode === 'function' && typeof $video.webkitSetPresentationMode === 'function') {
|
||||
def(art, 'pip', webkitPip(art))
|
||||
}
|
||||
else {
|
||||
def(art, 'pip', {
|
||||
get: () => false,
|
||||
set() {
|
||||
if (!isClosing(art))
|
||||
notice.show = i18n.get('PIP Not Supported')
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
import { addClass, getSafeAreaInsets, hasClass, removeClass, setStyle } from '../utils'
|
||||
|
||||
export default function autoOrientation(art) {
|
||||
const {
|
||||
notice,
|
||||
constructor,
|
||||
template: { $player, $video },
|
||||
} = art
|
||||
|
||||
const WEB_CLASS = 'art-auto-orientation'
|
||||
const FS_CLASS = 'art-auto-orientation-fullscreen'
|
||||
let fsLocked = false
|
||||
|
||||
function applyWebRotate() {
|
||||
const insets = getSafeAreaInsets()
|
||||
const viewWidth = document.documentElement.clientWidth
|
||||
const viewHeight = document.documentElement.clientHeight
|
||||
const safeWidth = viewHeight - insets.top - insets.bottom
|
||||
const safeHeight = viewWidth - insets.left - insets.right
|
||||
setStyle($player, 'width', `${safeWidth}px`)
|
||||
setStyle($player, 'height', `${safeHeight}px`)
|
||||
setStyle($player, 'transform-origin', '0 0')
|
||||
setStyle($player, 'transform', `rotate(90deg) translate(${insets.top}px, -${viewWidth - insets.right}px)`)
|
||||
addClass($player, WEB_CLASS)
|
||||
art.isRotate = true
|
||||
art.emit('resize')
|
||||
}
|
||||
|
||||
function clearWebRotate() {
|
||||
setStyle($player, 'width', '')
|
||||
setStyle($player, 'height', '')
|
||||
setStyle($player, 'transform-origin', '')
|
||||
setStyle($player, 'transform', '')
|
||||
removeClass($player, WEB_CLASS)
|
||||
art.isRotate = false
|
||||
art.emit('resize')
|
||||
}
|
||||
|
||||
function needRotate() {
|
||||
const { videoWidth, videoHeight } = $video
|
||||
const vw = document.documentElement.clientWidth
|
||||
const vh = document.documentElement.clientHeight
|
||||
return (videoWidth > videoHeight && vw < vh) || (videoWidth < videoHeight && vw > vh)
|
||||
}
|
||||
|
||||
art.on('fullscreenWeb', (state) => {
|
||||
if (state) {
|
||||
if (needRotate()) {
|
||||
const delay = Number(constructor.AUTO_ORIENTATION_TIME ?? 0)
|
||||
setTimeout(() => {
|
||||
if (art.fullscreenWeb && !hasClass($player, WEB_CLASS)) {
|
||||
applyWebRotate()
|
||||
}
|
||||
}, delay)
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (hasClass($player, WEB_CLASS))
|
||||
clearWebRotate()
|
||||
}
|
||||
})
|
||||
|
||||
art.on('fullscreen', async (state) => {
|
||||
const canLock = !!screen?.orientation?.lock
|
||||
|
||||
if (state) {
|
||||
if (canLock && needRotate()) {
|
||||
try {
|
||||
const last = screen.orientation.type
|
||||
const opposite = last.startsWith('portrait') ? 'landscape' : 'portrait'
|
||||
await screen.orientation.lock(opposite)
|
||||
fsLocked = true
|
||||
addClass($player, FS_CLASS)
|
||||
}
|
||||
catch (err) {
|
||||
fsLocked = false
|
||||
notice.show = err
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (hasClass($player, FS_CLASS)) {
|
||||
removeClass($player, FS_CLASS)
|
||||
}
|
||||
if (canLock && fsLocked) {
|
||||
try {
|
||||
screen.orientation.unlock()
|
||||
}
|
||||
catch { }
|
||||
fsLocked = false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
name: 'autoOrientation',
|
||||
get state() {
|
||||
return hasClass($player, WEB_CLASS)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { OrientationHost } from '../display/orientation-types'
|
||||
import { nativeOrientation } from '../display/orientation-native'
|
||||
import { webOrientation } from '../display/orientation-web'
|
||||
import { positiveSize } from '../display/sizing'
|
||||
import { getScope } from '../lifecycle/instance'
|
||||
|
||||
export default function autoOrientation(art: OrientationHost): { name: string, readonly state: boolean } {
|
||||
const { $player, $video } = art.template
|
||||
const needRotate = () => {
|
||||
const media = { width: $video.videoWidth, height: $video.videoHeight }
|
||||
const viewport = $player.ownerDocument.documentElement
|
||||
const view = { width: viewport.clientWidth, height: viewport.clientHeight }
|
||||
return positiveSize(media) && positiveSize(view)
|
||||
&& ((media.width > media.height && view.width < view.height) || (media.width < media.height && view.width > view.height))
|
||||
}
|
||||
const web = webOrientation(art, needRotate)
|
||||
const native = nativeOrientation(art, needRotate)
|
||||
art.on('fullscreenWeb', web)
|
||||
art.on('fullscreen', native)
|
||||
getScope(art).add(() => {
|
||||
art.off('fullscreenWeb', web)
|
||||
art.off('fullscreen', native)
|
||||
})
|
||||
return {
|
||||
name: 'autoOrientation',
|
||||
get state() {
|
||||
return $player.classList.contains('art-auto-orientation')
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { BuiltinRegistry, PluginHost } from './types'
|
||||
import type { BuiltinRegistry, PluginFactory, PluginHost } from './types'
|
||||
import { isClosing } from '../lifecycle/instance'
|
||||
import { isMobile } from '../utils/compatibility'
|
||||
import autoOrientation from './autoOrientation'
|
||||
@@ -15,8 +15,10 @@ export default function installBuiltins<Host extends PluginHost<Host>>(registry:
|
||||
registry.add(lock)
|
||||
if (!isClosing(registry.art) && option.autoPlayback && !option.isLive)
|
||||
registry.add(autoPlayback)
|
||||
if (!isClosing(registry.art) && option.autoOrientation && isMobile)
|
||||
registry.add(autoOrientation)
|
||||
if (!isClosing(registry.art) && option.autoOrientation && isMobile) {
|
||||
// The JS constructor supplies the complete host before builtin installation.
|
||||
registry.add(autoOrientation as unknown as PluginFactory<Host>)
|
||||
}
|
||||
if (!isClosing(registry.art) && option.fastForward && isMobile && !option.isLive)
|
||||
registry.add(fastForward)
|
||||
}
|
||||
@@ -3,6 +3,8 @@
|
||||
z-index: 9999;
|
||||
width: 320px;
|
||||
height: 180px;
|
||||
max-width: 100vw;
|
||||
max-height: 100vh;
|
||||
background: #000;
|
||||
border-radius: var(--art-border-radius);
|
||||
cursor: move;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,728 @@
|
||||
{
|
||||
"task": "CORE-16",
|
||||
"date": "2026-09-11",
|
||||
"status": "partial",
|
||||
"phase": "native-document-fullscreen",
|
||||
"parentCommit": "1b83081e8ffa893a17b14414c7698bfed7113b1a",
|
||||
"source": {
|
||||
"packages/artplayer/src/component/dom.ts": "740f63e6e5942c444b9f05605880f8d56a77b066dbe5c4791da74f78cb96c668",
|
||||
"packages/artplayer/src/component/resources.ts": "ff158ef0db8ba13343ff6ba329713820cfe2f5c2c446a7873a6cfff782855edf",
|
||||
"packages/artplayer/src/component/selection.ts": "1f0bd089415da28db862647ffd1a0b4af752576465fb616d4b97ae4f8dcae3a7",
|
||||
"packages/artplayer/src/component/types.ts": "d7b0e7008b0f6c82e3e97ac43e0188598dca4f740a860e7a60b3b38a46f4565d",
|
||||
"packages/artplayer/src/config/index.js": "d6d2bd612daaf6c5c6e982158acbdf724b16d3c35bb5f1a5c923284665a8de79",
|
||||
"packages/artplayer/src/contextmenu/aspectRatio.ts": "a25464e14216b2be392fcb3fdda2439e6d6f98d1962f0acbba30e3bd53bcb2d4",
|
||||
"packages/artplayer/src/contextmenu/close.ts": "84b7a96df910e6f38140df8b1e73519d209a7331e9be9f42d076ae791757024f",
|
||||
"packages/artplayer/src/contextmenu/flip.ts": "42c192d23e347d74e0bb814697a4d59eac20afbec102a282d5aa15780cab5f72",
|
||||
"packages/artplayer/src/contextmenu/index.ts": "4fd292a931b5bda9f16a1c2bedd50cd0d8c0b8fb6010ffc5faaad2c197d2d9e5",
|
||||
"packages/artplayer/src/contextmenu/info.ts": "f37cf0104510c13a130d2f55114c8f7ff15cc1b51863f97e60825442bdb2bf01",
|
||||
"packages/artplayer/src/contextmenu/playbackRate.ts": "e177f4017ea9291794fb4023715c81bfc01f927324ab9bd0e32c741e4a3b0ca7",
|
||||
"packages/artplayer/src/contextmenu/types.ts": "a93fc90f4e17b731e8f6b486a507117810b61c1f1b0d9e775eba27dabeaeee1d",
|
||||
"packages/artplayer/src/contextmenu/version.ts": "0553a716be0ba49f614059c41eef6dc694a9641fca79e6448b1b30382c6b582b",
|
||||
"packages/artplayer/src/control/airplay.ts": "a02ab41654a963e06fb2eb206cfe606cebc27252ba00b3fc2116ea232c8d7d51",
|
||||
"packages/artplayer/src/control/builtins.ts": "2a0e12a9741713c92938e1d2009149bbbde498284aa85c63f270d02b27dc22b1",
|
||||
"packages/artplayer/src/control/fullscreen.ts": "6856d98530877b9458275d8e064a94a81b9e2a4de079cce1a83e7cbe71e5ab8b",
|
||||
"packages/artplayer/src/control/fullscreenWeb.ts": "e20f8fe9788738adfccb21e7bf248e5e8068312e856bb2626d05fbb20cfdb65d",
|
||||
"packages/artplayer/src/control/index.ts": "ce23b79a0a40f22bdd8ad5f461d6b0df9366c7e4ab78571e0c67649f6e6d38e7",
|
||||
"packages/artplayer/src/control/layout.ts": "b5134a9d0cb5ad128b1fb229dce79a306b944f750d8620e224a99f5791e6cab5",
|
||||
"packages/artplayer/src/control/pip.ts": "9299d785ce21b16dc9edbcb6a803f8748157ed16e1924f461bdbc4853adaa5ac",
|
||||
"packages/artplayer/src/control/playAndPause.ts": "e65461f5bf29680f12b7e0efa7d44186f07bbe9936247a9a220f724c9d9774bb",
|
||||
"packages/artplayer/src/control/progress/interactions.ts": "19d6f73c21f75daaef109247bee392d82b46fecffd04be0944386c307496b250",
|
||||
"packages/artplayer/src/control/progress/position.ts": "1008c5129097cd2c51fefb56e88e9df6fa07f09ed5f4492505da9153d8284cd1",
|
||||
"packages/artplayer/src/control/progress/view.ts": "60ee16d5170be81b8132bb56dd0bc0eaf23e50f5c268d0697c9187b38a14aba9",
|
||||
"packages/artplayer/src/control/progress.ts": "f0876ddbdc01f812a5a776bdeefdff8dc52e557aff684f99b5ea7f6bfc8c0fb3",
|
||||
"packages/artplayer/src/control/resources.ts": "16fd7c53ac8f47b333041c4f298313390dc102523d47767b8a16f39a2564ba74",
|
||||
"packages/artplayer/src/control/screenshot.ts": "430dc1b0283bc8a5c5396539b291dce1197470611696b32fde4b2c56d2db6b1f",
|
||||
"packages/artplayer/src/control/selector.ts": "5d946c36d0308f32e08dd0e189f0c959f738ed7ab521230833c76a5bf9ca6478",
|
||||
"packages/artplayer/src/control/setting.ts": "63e9824ffee2221ba82be9f2bc430b9b3133a2186905fcce2a61b8aa946a3d8a",
|
||||
"packages/artplayer/src/control/time.ts": "fa572092e0848878689367fb536e9658d13888d0a0d476babc5acd165af0f8d3",
|
||||
"packages/artplayer/src/control/types.ts": "7fab47c7106efa51aae89026af335f74d9f3ea52ef9864ec2801d0ea2deffc4c",
|
||||
"packages/artplayer/src/control/volume.ts": "b50d1a5d2697d6440ac0f3d82634b13f7c2fff00c185a1adf4451ed8ba810709",
|
||||
"packages/artplayer/src/display/fullscreen-abandoned.ts": "1715b30d336976921b4957b444418702b7d93dbf1debce47429269638d15810f",
|
||||
"packages/artplayer/src/display/fullscreen-adapter.ts": "71da10ec21c7706a8264badedac9022c74b865887931e9e369168fe3233c3b2f",
|
||||
"packages/artplayer/src/display/fullscreen-request.ts": "67015be4f49e8df280ad5666a2242c8fcf13ddff64e3c0c47fd2d67f53e486b1",
|
||||
"packages/artplayer/src/display/native-fullscreen.ts": "be4911938a4cc708b96043550c03176188a6272fad2444921555801268535c5c",
|
||||
"packages/artplayer/src/display/placement.ts": "dbe4491ebd0f46b4efe13ac567e476fcd9ca5b84da675f19025935b5e3f8d733",
|
||||
"packages/artplayer/src/display/types.ts": "f6467708487b7999d4ee1c2ec511c7d49d972b05943ef11c23f30fdeac868366",
|
||||
"packages/artplayer/src/display/web-fullscreen.ts": "ef3a29a058133395e5b1eb36ffb742290ff30cffa3f22ac690a9283a8a566678",
|
||||
"packages/artplayer/src/events/clickInit.js": "bc680c921a958c25705962577513677d5d97de94e4eff6a04ba20d2f60e6a1be",
|
||||
"packages/artplayer/src/events/gestureInit.js": "a5dcdb4216c8e3544fc85a1b4d3ebd929ec6f7d51ab17f2cee713030ae7ad464",
|
||||
"packages/artplayer/src/events/globalInit.js": "5d8c3147f17755b0d35504cfc69cf8cbff510b66b2a52686d8f2a235a6c5b9df",
|
||||
"packages/artplayer/src/events/hoverInit.js": "7ed5666e01972dc0955d1c1b4006001c99dd55ed133ca026f685b71d6b4d01c6",
|
||||
"packages/artplayer/src/events/index.js": "94ddb9ad5ceb21901fbbc974d983fec60c488314474c89138e4ab2b40e205d5a",
|
||||
"packages/artplayer/src/events/moveInit.js": "782d627260c9fdf46c0766876138f05836860bc9c2fa2c519d22b43f3d24de15",
|
||||
"packages/artplayer/src/events/resizeInit.js": "26fc9628490e6c45ef741e7bc7c1353097b21da1dc83aae65dc80783226e36bc",
|
||||
"packages/artplayer/src/events/updateInit.js": "1b4ef85c819ae68eb0de1791241b2c3d4e22bb108746518fa67f7c322591e55a",
|
||||
"packages/artplayer/src/events/viewInit.js": "2b4ce508812b8f03cf5801a5e3a7eaa4a5a8b05d6be500d217aeb6231fb93a51",
|
||||
"packages/artplayer/src/hotkey.js": "689fb87b2ed8a424cbc2880c8f70610b3695dc5b0346c9a5833699996682c0d6",
|
||||
"packages/artplayer/src/i18n/ar.ts": "c1e41787c700096ecbc91de6558353f43d8930929ee312b7aa2c7da14a07ec75",
|
||||
"packages/artplayer/src/i18n/cs.ts": "62c5ffcf3ac24e0dc2d7222b7ebb2b4d3454e9c04b80930a50defb3867d645be",
|
||||
"packages/artplayer/src/i18n/es.ts": "5d31184b41ef29c1544c91ff08e3bbf9fc5c5776f0af0aa0238f90a9d161b9b1",
|
||||
"packages/artplayer/src/i18n/fa.ts": "1294b91178ebdc7bc26d14a6f407ba8598207ae0dc812f3b821b62b7bf0089e5",
|
||||
"packages/artplayer/src/i18n/fr.ts": "7125c0011b90f507d5b5fe20afd875992b62af9cdd44f02bbabed051c4ebc107",
|
||||
"packages/artplayer/src/i18n/id.ts": "0d2e79cc0118cff6be83786a3fc048461785a64bdffc6bc45ffb68cb595f6da4",
|
||||
"packages/artplayer/src/i18n/index.ts": "09c6f5bce58e4118dde10e8b2a2d3808785b7fde913a54dd189e0b473e21266a",
|
||||
"packages/artplayer/src/i18n/pl.ts": "f6fb58a0d3fa118e22cfa6df5edd74c7bf39dc9ff1d416fb2b01e1bb8708b06f",
|
||||
"packages/artplayer/src/i18n/publish.ts": "5b216bbe313b3cda56a5da41fba4fb9d095eeec0dc903fcf284ddd8f94926e99",
|
||||
"packages/artplayer/src/i18n/ru.ts": "473c43197acec10991b0f67814f79421dfe9ec4f534fb15413a32cf2a4d8ad38",
|
||||
"packages/artplayer/src/i18n/tr.ts": "7b6afbbe452c4907c8bef36663bd218fd140bd1a4b3c776f31c53415e78e8f16",
|
||||
"packages/artplayer/src/i18n/vi.ts": "29ea7f23d7400c847596632c86a4ca10c39cbafe1e6d723ed099b819aca7e764",
|
||||
"packages/artplayer/src/i18n/zh-cn.ts": "22584521b0aa92da5278b01581e6e8e5bb3a23a2c3029e9df8642b7f0bded392",
|
||||
"packages/artplayer/src/i18n/zh-tw.ts": "8da94a4fd508176bbb6b7ce2f4ba6ef6a56e7b1229bc67cc42d1a7bde70a7e9d",
|
||||
"packages/artplayer/src/icons/airplay.svg": "8c75a40f1bfacee2be2a425e132ff51f3712eba60679010f7b1349d4a04374bc",
|
||||
"packages/artplayer/src/icons/arrow-left.svg": "8b9d544b7bd728d01747005fc7a2d63f822eed3b605393e2435b6733861b585f",
|
||||
"packages/artplayer/src/icons/arrow-right.svg": "68c63d6d5810173ec9192c9e96ee7b6aefdc3b79d3c1329fb4d6c750f859fe10",
|
||||
"packages/artplayer/src/icons/aspect-ratio.svg": "987752a87701523057d8dfa7c45a6a139c4542b96fdcfaf8665722bbeb41f3b2",
|
||||
"packages/artplayer/src/icons/check.svg": "963ce0aee2337fdee951aa6c917cc75ee0b4e75158887ccc16baebfd3925942b",
|
||||
"packages/artplayer/src/icons/close.svg": "ebabfe8044f48d5df4d13f4f906a163fd55a4d6f1dedb3b5ba95601209bc8888",
|
||||
"packages/artplayer/src/icons/config.svg": "e1884d5411296b5cd1bfec7803b558b0afa5ab64478393977b07a662e2e0f15c",
|
||||
"packages/artplayer/src/icons/defaults.ts": "31df862d754922fe5d68b20911c477d7c22893a6a67b5dcb1a48d3b437a0b23a",
|
||||
"packages/artplayer/src/icons/error.svg": "b40c01e511a599c9dd369aeb9a3b2a0d1a76ef3c5ede8124cbbc23f8e4e7248c",
|
||||
"packages/artplayer/src/icons/fast-forward.svg": "abef6fe4d75ffa74d3a84683373b4702a4eda2a5b4045952f969ee62cc68b0be",
|
||||
"packages/artplayer/src/icons/flip.svg": "474a9d53a97add71e3fb7bc6530a68580ce63a1d83f9b75d1c2908a7cf494152",
|
||||
"packages/artplayer/src/icons/fullscreen-off.svg": "71125ac56586897e2c0e52035a4d65cff34cf52043a95b45d186e0d268671399",
|
||||
"packages/artplayer/src/icons/fullscreen-on.svg": "d46cdf265cac729c9b7382722ebe7a4c76d71ad4e5a27c2d5ed8323fd595ebf5",
|
||||
"packages/artplayer/src/icons/fullscreen-web-off.svg": "52a6862ecd7cba12ab7042de5064c498bc2e9f86b430358c912401fafe4f4a98",
|
||||
"packages/artplayer/src/icons/fullscreen-web-on.svg": "5cfaae62b61456251e771bff01356fad8376ebedb10a4b8819740f60e6e5a073",
|
||||
"packages/artplayer/src/icons/index.ts": "3ef720aee403e6b7baf1ab472dbdd16c671f2d691ecea48ff1c25ca1ffc66296",
|
||||
"packages/artplayer/src/icons/loading.svg": "485472153890eff3ed49963f7bf7d841f66ca2f40894c01d6e8bcf1bf4ad1c58",
|
||||
"packages/artplayer/src/icons/lock.svg": "431e9a25fae051016be0efa286387ec3e5f7651c3ecf569bf0ece9842f57156c",
|
||||
"packages/artplayer/src/icons/pause.svg": "57405dba81dd3d9f329d957fdc704101cbb6f6654325eb0083c03324c4453a68",
|
||||
"packages/artplayer/src/icons/pip.svg": "1e922140e0941e5b9ef53dc0b4776813e1c470ea229fe3ec786fd148148c3352",
|
||||
"packages/artplayer/src/icons/play.svg": "41a961c629a55cb25a813e115a25204e492d4e935a0879177e65d6e1ddfd6796",
|
||||
"packages/artplayer/src/icons/playback-rate.svg": "872deecac0365ab04aeb5ccb1cdd9a0c88589a1442709fdac7c0ed12a241ff2b",
|
||||
"packages/artplayer/src/icons/screenshot.svg": "538b5cd0c6b20afb98bd850e15019072d652d191075c2869f2fc0f28b597b131",
|
||||
"packages/artplayer/src/icons/setting.svg": "2976b00d1f03e78f28d0e55ebcb01d5a0746ec1a6ff8de97a5e8294bacdd1fad",
|
||||
"packages/artplayer/src/icons/state.svg": "620a127f74bb5e249993bd7008c027373263eacb4a44af8adeaa0d7523346887",
|
||||
"packages/artplayer/src/icons/switch-off.svg": "95a8d81a4a3bfcf7c016d5f71d46a72be73c5e94790c55fd1d31023416ef5deb",
|
||||
"packages/artplayer/src/icons/switch-on.svg": "cccd0f9c1e54d9ac50c323360da944d8ff44633a82ff17889914b6bc8b1652ae",
|
||||
"packages/artplayer/src/icons/unlock.svg": "fc058db3d99a475fb9ffbbbb6248978a95990a9c532b83d0955ac28eaa626c60",
|
||||
"packages/artplayer/src/icons/volume-close.svg": "f7e100fd9e9f3f6e06afe5bc994d5d9305ee4d1ee23fae37e4108cf286dab379",
|
||||
"packages/artplayer/src/icons/volume.svg": "3a78f40d8b48595e8f15d9189e4d767e0c7abbf0338d3f2db1ac810049dd3942",
|
||||
"packages/artplayer/src/index.js": "cb874ac34fa650bd364b3fef7eed4830bf3bd47093097ae7ba0432e7e64b1a10",
|
||||
"packages/artplayer/src/info.js": "6dcdb4457d63ddb74fb6268df64522722420d6c0eb7b1654d6fb7703466d3b8c",
|
||||
"packages/artplayer/src/layer.ts": "4014268eed043d5dd7b925cdeef6245e357cda67192a1301c5612ef98cdbec12",
|
||||
"packages/artplayer/src/libs/screenfull.js": "2332b50c0c6364d5c99080be0f5739255e4b9e465d2cca40f761adce9a1908f8",
|
||||
"packages/artplayer/src/lifecycle/instance.ts": "19a89de4ae22cb57eea7302265ce07422d12d1aea5acb99deb44fd0dba351a13",
|
||||
"packages/artplayer/src/lifecycle/resources.ts": "399ea5e84545c973f8967e8ffd7326dad7b3168ca6f96f77286995a5b62ee389",
|
||||
"packages/artplayer/src/lifecycle/scope.ts": "e2188e8544b27cf8c7ac149e7146af628a2d6166c82b719a247adef5d85be5db",
|
||||
"packages/artplayer/src/lifecycle/template-rollback.ts": "7a83da6739bcec1587b02f8157934988910d7f9103fe489a0d6f4f65290c3d77",
|
||||
"packages/artplayer/src/loading.js": "4b08a8f9fa69f71d054cdf1508bbb9e92c075c8df1eda0fa8a79efbf08eb8c40",
|
||||
"packages/artplayer/src/mask.js": "094162e28ec76c81f73bb7bfe276bf5832080ffb7aa9d9dc9dc013479659c2e5",
|
||||
"packages/artplayer/src/media/events/forward.ts": "6b1b7e80036bfb523b51843dc6ec6d83c2b949cefb9cef085b2d679a444e46d8",
|
||||
"packages/artplayer/src/media/events/listen.ts": "4eb7e943c1ebca77da8025d83b7d408276187cd02860b7793284344378bd0fe0",
|
||||
"packages/artplayer/src/media/events/playback.ts": "2c68e4f6274a0dc65e56d42c928e500fc1d63bf3e03bbdd8a83c3f3ed7ae8ee4",
|
||||
"packages/artplayer/src/media/events/readiness.ts": "3ba74f26dd9ff0729967097c2cb6f329a9cea57745bfe8173f9173e0d134476a",
|
||||
"packages/artplayer/src/media/events/reconnect.ts": "876027b5116d0d3628755705682b30630f4b713199ed8015541932d81d2944be",
|
||||
"packages/artplayer/src/media/events/types.ts": "5532f57129032a9685305d0d319543146c3e8af134ac12c8aa521cca9e83a6c2",
|
||||
"packages/artplayer/src/media/hosts.ts": "7129cb93290a250e932464b71a4e4abce8a98e83935d4fd6c36a59fc6357b6af",
|
||||
"packages/artplayer/src/media/playback.ts": "b0e8ae22f2660249361bf05bb2333685e14f5a7ac3e9ba0c62ee7efd38338b93",
|
||||
"packages/artplayer/src/media/types.ts": "96d4a7f066cbb6fb9138165dd736b4ca500cb19c827bd7702134d61c44cc0e87",
|
||||
"packages/artplayer/src/notice.js": "72e97833e0499993d8d133111e32f32f031b911426117397be2003e824023664",
|
||||
"packages/artplayer/src/option/defaults.ts": "f357ef80cf32085fcbc3409d92dfa27b31c13fbcf823f576052c0347911c0cca",
|
||||
"packages/artplayer/src/option/resolve.ts": "53a73627cd540e3f21349c0a43beda35ea5195647a31e3f5b30ac292390b4b1b",
|
||||
"packages/artplayer/src/option/types.ts": "f2ce01c01acf45fac6cbb34685b8e6c321bcad6b0da2526f3f189eb5962ea0e5",
|
||||
"packages/artplayer/src/player/airplayMix.js": "b9ce49489e75045b6a1df4d8ed28f123c1b105ac4c04fefaabcffe4353ad004d",
|
||||
"packages/artplayer/src/player/aspectRatioMix.js": "c0a4cbd84bd0d70f078d87fc2ed082ee81fd33eed12b6265fa2d6f043b8f0f97",
|
||||
"packages/artplayer/src/player/attrMix.js": "c873d587ff71e024cfda5b5464d0c70a09a6a319da3f18fbaa7451a16c9ae055",
|
||||
"packages/artplayer/src/player/autoHeightMix.js": "e7400a46eeb01416d5ac7c6b787295cd9b9f90b38ce640a737ef5a34a8affd6d",
|
||||
"packages/artplayer/src/player/autoSizeMix.js": "341e8c6a6a1b451c43f758a7774d1cd6cdf9e1f9e818692e03eb58c2fbda4525",
|
||||
"packages/artplayer/src/player/cssVarMix.js": "93e641dd760e8f844a643a0b7d8af92b73a7f205e55087d9501934d218701bd2",
|
||||
"packages/artplayer/src/player/currentTimeMix.ts": "78c749ec4dc94f4a3a0ce02a87156f07894401b64d12c6c7c3cb1192ff266902",
|
||||
"packages/artplayer/src/player/durationMix.ts": "60a9d665cf302dcb6b3e37e2f057e31720935e5e588fd7709231f691f6014191",
|
||||
"packages/artplayer/src/player/eventInit.ts": "a915c5a618e70b1ab10536b1273ba0fea4ec11d3a2ca629b538e90b5ad2b55f9",
|
||||
"packages/artplayer/src/player/flipMix.js": "f5abb8d079d0a9c4f9eaa31c4940723bf407ba422ac713dc73443dfa6cb6ac4f",
|
||||
"packages/artplayer/src/player/fullscreenMix.ts": "118c5b1c6efa26ee1fe2e0451e64da8df370d80b31034dfe8fc11ba2c8b955a4",
|
||||
"packages/artplayer/src/player/fullscreenWebMix.ts": "75b83a6093c7887ca8725f63c55503d0bf3aafdbbf2347b579120cb257503555",
|
||||
"packages/artplayer/src/player/index.js": "c3eaeab0aa5015a51dfeeb4de6f25b02b8cd66e1f8471940367d628b5fd0bdec",
|
||||
"packages/artplayer/src/player/loadedMix.ts": "6a4c4a08ed74346dad3e138b1dda22c8c9c12f175fe6d00da41b89c78cbe2c18",
|
||||
"packages/artplayer/src/player/miniMix.js": "7355a66a23de47be91ab08dd28e8d54116aa2a603bd47b687014e4ab4736094e",
|
||||
"packages/artplayer/src/player/optionInit.js": "9d1d9d276da2403a580498c62f947373e000d9cd6e4bef5831d272c3055b5592",
|
||||
"packages/artplayer/src/player/pauseMix.ts": "5bbb68eab7dcf2170c944cabb8d4b82be4e6d7623e37ad6ea464b81c1028bc0b",
|
||||
"packages/artplayer/src/player/pipMix.js": "15322751e235a70e6f9dc4d5e2b6b0ee6a8b93080370c6a90cd96268e101a8ab",
|
||||
"packages/artplayer/src/player/playbackRateMix.ts": "e185c6ff52cd567f7fd7c30553bbd6c65517324d1d16c4948c3623ef457ef536",
|
||||
"packages/artplayer/src/player/playedMix.ts": "5b6b89bf8dec1efb19cd51e46af1505bae006bdb3fb798e9dec7e3fb5cf0acaa",
|
||||
"packages/artplayer/src/player/playingMix.ts": "968c9f6e6d23c233c77d62a593db64adcc6d1a2411f5f1c61a35c6996d6a54d9",
|
||||
"packages/artplayer/src/player/playMix.ts": "60249fcacdc5b1bc6f33bcd806e8a4f5472cea21d40550b042794a75027fe34a",
|
||||
"packages/artplayer/src/player/posterMix.js": "a96b0a9f5817919e373a5a55b43fe7ea94af357051c01ddd6ce9c68bc932ecba",
|
||||
"packages/artplayer/src/player/qualityMix.ts": "8dfcb94f695cf983f313f00abe1aec183ceb2af4b398fbf62bf1d114257a8f43",
|
||||
"packages/artplayer/src/player/rectMix.ts": "a741ee09fa3271df84363c5d73eac1b2e5adb39491f95f8112af81f1219ffed4",
|
||||
"packages/artplayer/src/player/screenshotMix.js": "c64871270b224233ea67ff31c3fbc0c2e2eb35635da5e4d5a263b930fb96e6e7",
|
||||
"packages/artplayer/src/player/seekMix.ts": "e6e1bf00e8441f1ce58d2a67a66f7e5e98c86d78620ff10a5e6268d54f13fadb",
|
||||
"packages/artplayer/src/player/stateMix.ts": "e45b3209a93ab14665d19c5740d5c632187e9fe234b019a2a0263e26884ab3b2",
|
||||
"packages/artplayer/src/player/subtitleOffsetMix.ts": "e7866cf3a6d9fe271a4c34973f7a961542a738e89a3647df85197cee5853ce8b",
|
||||
"packages/artplayer/src/player/switchMix.ts": "b47e174ef0edb2b96ff29f2ddd3c9f2dd2a38713db2a81002b18c89ce71994e9",
|
||||
"packages/artplayer/src/player/themeMix.js": "382acfaf113fbd1ee77c63dba72953d984ea715f2d99748a12b9eae70c081d49",
|
||||
"packages/artplayer/src/player/thumbnailsMix.js": "43bdbd9b62a803f4966103c6718305a9ef665d25df26f735d3e34558ddf339bf",
|
||||
"packages/artplayer/src/player/toggleMix.ts": "a3050136248e7d0b942387484c61286ac2b33a90724b1dc3460f5061be76f247",
|
||||
"packages/artplayer/src/player/typeMix.js": "70920c8f785dca6d4bd66d82b996c37f1b69265190d7747ae8de298afae3604e",
|
||||
"packages/artplayer/src/player/urlMix.ts": "23ee05577af66e3608f9c6d63f060bd3de68a99b1992e4e35e45f16a08a8c14f",
|
||||
"packages/artplayer/src/player/volumeMix.ts": "d830bb27b984e0ab2f687943fb49abcbd0331d71fe96049fd81edbddaa64f07f",
|
||||
"packages/artplayer/src/plugins/autoOrientation.js": "6ceb81bb3a778323f88ecaa78ef5646b642dad3f7d3176031c4ce53815a8d153",
|
||||
"packages/artplayer/src/plugins/autoPlayback.js": "26f4a21691a2b2f767304b7d2fe40a35c599575651b9ace0c55a709a2be279b8",
|
||||
"packages/artplayer/src/plugins/builtins.ts": "4b9f83959ebaea9991636ee670c3344a5ede5990d1631177ca470ecc156ddc67",
|
||||
"packages/artplayer/src/plugins/fastForward.js": "d1f396efb22a279f9450fd02f85dc2245d274dbf0d36c99e87a5e5f895cc0029",
|
||||
"packages/artplayer/src/plugins/index.ts": "28d13abc95cbedafadae0fdb682bf86fda9b705191c51843943a5dadddbd0a53",
|
||||
"packages/artplayer/src/plugins/lock.js": "843caf8c7981463e8dadf1510cd028e7e70049f5a10038bd66dfccbc63bb7886",
|
||||
"packages/artplayer/src/plugins/miniProgressBar.js": "3bf48d30c209105a0144c17062d6ea4e2bdd351b1bc928df73ea1680b78462e2",
|
||||
"packages/artplayer/src/plugins/registration.ts": "9891686c32aeaa33c48ca500cf30ab24ffbb0835c6248769b4c645a5ff20129b",
|
||||
"packages/artplayer/src/plugins/types.ts": "6823098d5ec1bf053f60ddfdd36bfcd3f06f3b528cc5dd30fd676a1318dac590",
|
||||
"packages/artplayer/src/scheme/index.ts": "dc4a3b5e08478cf72b46b0b59c023359ad5f79b891ea8a6a119ce24a5251821a",
|
||||
"packages/artplayer/src/setting/activity.ts": "699ceed4bdeddb6d48eacf79c8dbf737f5d2d8c4983be70205322b14807a91b7",
|
||||
"packages/artplayer/src/setting/aspectRatio.ts": "a16eb58fadf77f0a9009cf162d957ffd59d9b8fde83cc193d510fd642e3456cb",
|
||||
"packages/artplayer/src/setting/checkpoint.ts": "9d3bff311f9b15c067ca49661af4f48de939f266b8077e7c2f0d66edef22dde6",
|
||||
"packages/artplayer/src/setting/events.ts": "f7a3347ca146232caf2a8c9a5a18731cf7bc91dca4cd36370e0265c6eb70df53",
|
||||
"packages/artplayer/src/setting/flip.ts": "e72a4ae33300d9e48880897a4f85a04fa6d9a5fff7b06ecaf9081cfb1bb14404",
|
||||
"packages/artplayer/src/setting/index.ts": "f1e8a302077a8ccfa77c17ce344bdd6816943e77b5876c7f623314ff846742f7",
|
||||
"packages/artplayer/src/setting/layout.ts": "584604e8fffaeaf1d633d1ed45e64d94595c2a56976e3a3f7ede01839b83a37d",
|
||||
"packages/artplayer/src/setting/model.ts": "c47348f0f9494c4ce8ea5561bcde6eb698fb11e9197ccdc1ca1359a6d5e370dd",
|
||||
"packages/artplayer/src/setting/panels.ts": "64d48720971763985a2a652a18c2ed2f32300c3553d56b52049ae40c4dfceae1",
|
||||
"packages/artplayer/src/setting/playbackRate.ts": "2e70e0d2a33cb0ec9fcdd6d22424979fed7b2e01f87015d6be4e520d5cf4e88a",
|
||||
"packages/artplayer/src/setting/registration.ts": "c29db62b5d90e0efa4a926c753015e65e96a47add3f5b8840ee51017052edf7c",
|
||||
"packages/artplayer/src/setting/render.ts": "2bb29994c60d0c47517bdf7cacb6afac7d65075f0a67d895a3ce52250b668232",
|
||||
"packages/artplayer/src/setting/resources.ts": "64c28cc2cb08cb87b0864ab957d7999bc94931be4788f282d6f190b8806a81fd",
|
||||
"packages/artplayer/src/setting/selection.ts": "a1bc04b99bda45cc1a2afcf2dd0925c88ccb55e169b971105277883ae2ccba0c",
|
||||
"packages/artplayer/src/setting/subtitleOffset.ts": "86513835d4e542ac39e9d551507473e3e14c71fd0d5ea725267008ecbff6ab83",
|
||||
"packages/artplayer/src/setting/types.ts": "799e7adeac76c4a327201ec66f10d8f25f92387409f1e5e65f265df7711973e1",
|
||||
"packages/artplayer/src/setting/update.ts": "3725fd69b369e4a345b1657ab559a915ab967c20088a093e288ffa7370cda264",
|
||||
"packages/artplayer/src/source/listen.ts": "c0596b98258c1544c8e4b762509646428450e56588da36908cb8583b8b716c37",
|
||||
"packages/artplayer/src/source/operation.ts": "a6cb65594a5b28247e3a6889fe0a2f47a34aeb08cdde5f0125800a87862cd559",
|
||||
"packages/artplayer/src/source/switch.ts": "697ab56a765f957e3ce1da076c69cfc11dbf0901efecae0199cd94c729e0b236",
|
||||
"packages/artplayer/src/source/types.ts": "9456d3d2f798188b730d99ef015e8a0a145598b0fe3ad1036b84f4a7cc1c5e6d",
|
||||
"packages/artplayer/src/storage.js": "92067265b7f1104c4680e27e35726cf033811c6551e7c38d66028f2dd5300491",
|
||||
"packages/artplayer/src/style/artplayer.less": "4e8513138d5d10c05b66986977d24ed17f1de3c0df3816dec6d90db7a97b5da3",
|
||||
"packages/artplayer/src/style/aspectRatio.less": "091931a0cdc83e65a669332811ef3276b85bca9e55f7fff42aa46893d728c1ef",
|
||||
"packages/artplayer/src/style/backdrop.less": "6c2cd0fa200d1f3525df98c3c56dbdc553445724aece3feb0386949245813343",
|
||||
"packages/artplayer/src/style/bottom.less": "6fb307d8449fd04b804d5e72ea0f175f8aa9c9dd2bf1efbf756f800b39ac97ab",
|
||||
"packages/artplayer/src/style/contextmenu.less": "70f6c7e26dc96652b659c7bf6fdad92737ed477dea69d253c57dc7266c76586d",
|
||||
"packages/artplayer/src/style/controls.less": "eb6d4bf4e4ff903d724d45f3ff60f0cf10c02e622fba195555a8179100cdce87",
|
||||
"packages/artplayer/src/style/danmuku.less": "e4215ee9fa2f1d0599e7311a2415cc975138265b7c6af0448d27d21b6c9e3511",
|
||||
"packages/artplayer/src/style/flip.less": "5d3c7a81df9be455c2f683ebf48f39597d218dc2144addacb72ee1736bac3b7f",
|
||||
"packages/artplayer/src/style/fullscreen.less": "dd350d3a0fc556d59b622a53c786b8c2a583e8fb28176e4b9e91baff90479496",
|
||||
"packages/artplayer/src/style/fullscreenWeb.less": "d74e785c2fbbb2976e131620e751326f622f2916eceff9f2333059ca3296cdc9",
|
||||
"packages/artplayer/src/style/hideCursor.less": "09a4607c8b0005dc199828172aa8dda395f861e407769b9696d793476c4a7823",
|
||||
"packages/artplayer/src/style/hint.less": "ae9f3115dfdb1e2d7b8249497c080e6a25f9667f915f6e89820338d67bbc2d85",
|
||||
"packages/artplayer/src/style/icon.less": "52d8f75c1d193e6160f57ca6997c3f9135adad8eb2338ed60c1eb8e5fe443be5",
|
||||
"packages/artplayer/src/style/index.less": "ec67999b458d5545a4f17eeda922872823d655da012fa69113902629ad847548",
|
||||
"packages/artplayer/src/style/index.ts": "856afc037ffcd4d18c4b54901ddbb30e310014553fe9d19560e34fb54b8378c1",
|
||||
"packages/artplayer/src/style/info.less": "fa56d1b8c8936218e581b2426026e4ba85cd5fea20d045c4f69aca96be4059be",
|
||||
"packages/artplayer/src/style/inject.ts": "9cddbe51b5de0da74f27c045ab982b509c3854cc6b81eeb02a6b974914760a4d",
|
||||
"packages/artplayer/src/style/layers.less": "9653c581190bb22d60a706109819cd010908898740701c9526957b5682991c5f",
|
||||
"packages/artplayer/src/style/loading.less": "185b0f05274196ba71fdb1d088b55af568f3342a495490c99c3f0e058a40659e",
|
||||
"packages/artplayer/src/style/mask.less": "d1239a171b71ea6184cb4aee6660d74d6ffc0c48f7c47c3cf5fbe52d2e93ac92",
|
||||
"packages/artplayer/src/style/mini.less": "a352e7a63f9529f18e6e389202fc277c500bc70acb38c7aa6eea9d7dba831c0f",
|
||||
"packages/artplayer/src/style/mixins.less": "c6be95aa682f266749fe37cb89796fbec6488abc537174f817da79bdac6e00b5",
|
||||
"packages/artplayer/src/style/mobile.less": "ff3f64fca5d2dd7ddd50385d88d9b2fa1a645909488ab7a0978ba4f1670b7c0e",
|
||||
"packages/artplayer/src/style/notice.less": "72a935c0effc557a9d7ab3baccb2c3b77fc3a1783450b4e3178a40a5325fe369",
|
||||
"packages/artplayer/src/style/plugins.less": "589d05e12a62e04fe848b36ba748433d23f0b909a04e861b1d6737f2799be183",
|
||||
"packages/artplayer/src/style/poster.less": "36b38fe138f8c9537ff4e93f7ec4f2dbdba1ec41d243d741ddf1da7c6581e546",
|
||||
"packages/artplayer/src/style/progress.less": "59b9d95d49dbcb1be40752d7086239b6de5925b0574da231199d999e22ff4990",
|
||||
"packages/artplayer/src/style/selector.less": "295012ba379a7e957a24476616a0c3623d6a06e82ebbdaa45b916dd6412d7f4f",
|
||||
"packages/artplayer/src/style/setting.less": "743837b3c84fc5fee40120598bd484c10bd4202b371002709c269a700919dda2",
|
||||
"packages/artplayer/src/style/subtitle.less": "273cacb63ec0214ba434dcf19a4a455dbbdd408fb2d1749de4878804c1371e41",
|
||||
"packages/artplayer/src/style/variables.less": "703739b2bdfc77bfde063d8788ef6b6a55b8730764baae8e7d755b7c54d8ecc7",
|
||||
"packages/artplayer/src/style/video.less": "b89c6f6e27592d49825c68c9ab98d46e81cb1e5add855863f89d19637f98e35f",
|
||||
"packages/artplayer/src/style/volume.less": "a3954794a9478d3872202b90d349c873cbb51639dcd7d1e3d73a55ce635fcd99",
|
||||
"packages/artplayer/src/subtitle/index.ts": "b4ead680dc82fa47c53d1ec7e5fb8bf94abbf55730d5d1130ad8d544cb42adc8",
|
||||
"packages/artplayer/src/subtitle/parse.ts": "7db540c5edb1d2312eed82757fd15a7ce6a28996ba20d0087e56730f21dede74",
|
||||
"packages/artplayer/src/subtitle/render.ts": "96493fc35680af635a8db225a8bf002208df79ed48b1d4ee6cc73587543add5c",
|
||||
"packages/artplayer/src/subtitle/request.ts": "543c964571bd5ab984bc1adc9f64a8917dd134ed2892ad10b30ed9c859f34119",
|
||||
"packages/artplayer/src/subtitle/state.ts": "9f79b982d66747170bc39bdaf99b65676310965f23da62a4dec15547c756a56b",
|
||||
"packages/artplayer/src/subtitle/track.ts": "707f4c43af4ebf4b87521f6f577168460e739274d0cec28b9efe5704d2540d18",
|
||||
"packages/artplayer/src/subtitle/types.ts": "e56db829d7588f3a5dfafd6e8377f93f0892cfe442710be3740e1136812dd2bd",
|
||||
"packages/artplayer/src/template/html.ts": "351c472bfd01fa5ba1a4e347ed31bd5faaf823d9b4c5cab1ce8c651e3b175021",
|
||||
"packages/artplayer/src/template/nodes.ts": "1572746a636647e4d313cc8752d3a110ff81fddb390ad7b0feb6a0db0d42b9d4",
|
||||
"packages/artplayer/src/template/types.ts": "23f67e78ee43f9ad9099c94f28e51c6113b167603ffbbcf2d6ca2b68cc40b803",
|
||||
"packages/artplayer/src/template.ts": "05c1499adc5dce27addf5761bec58d803f526d9b5b62c367c631e4cf2991391d",
|
||||
"packages/artplayer/src/utils/compatibility.js": "51290838ef62be8e4251f4db8ecdd45a626cecd8c732e4e53a1c55df8d454a13",
|
||||
"packages/artplayer/src/utils/component.ts": "d04a66213406ae3223326c2d470363e197dd1540519caa632f9d06378af989cc",
|
||||
"packages/artplayer/src/utils/dom.js": "a625a0d832f717fbdf186099d1ace1f0dd51f1ea871417ffa856a7186206fb91",
|
||||
"packages/artplayer/src/utils/emitter.ts": "26a93d3ff35b53e71a5f3bb22c252a854559d8151ede50a66a28d9eefae6c008",
|
||||
"packages/artplayer/src/utils/error.ts": "ba303c7d4e4228919088f9d608fa364596f5853ee646eda2008deee354866f38",
|
||||
"packages/artplayer/src/utils/file.ts": "ea71d7fc0e818d473bf5a596211ba9d571e4febdf1878014eadefa8d0a98d4cc",
|
||||
"packages/artplayer/src/utils/format.ts": "891df5b7542b713aef46bf2b93924f42e68eee574afd802db3e6c62ff2655ede",
|
||||
"packages/artplayer/src/utils/index.ts": "5211af833dddb2937d25dcc772740d62f9d5c5039ce5505bffff0796d4afd756",
|
||||
"packages/artplayer/src/utils/property.ts": "c737d4582a4382f4038d12dafdf8b43418e508c9b8b859b2f0cc17c20f370343",
|
||||
"packages/artplayer/src/utils/subtitle.ts": "f058851d11a418e321a8a498472b67fa535fec2c7b5d3df739b6b4f263d00337",
|
||||
"packages/artplayer/src/utils/time.ts": "b8ce3d50865f6c343e8e3ce9f5c87de565a87b4e9fbcbccbe7030e30e80b93fe"
|
||||
},
|
||||
"artifacts": {
|
||||
"packages/artplayer/dist/artplayer.js": "3d07659a29f5fc0d62615f474428d5a850db6a0cc7d5c4f5877fe083218182fc",
|
||||
"packages/artplayer/dist/artplayer.legacy.js": "c80c7b4b9e7d7929b112b9eb6bcc3889815d924e6035fe96820f7d89571fe16c",
|
||||
"packages/artplayer/dist/artplayer.mjs": "bfdb23a5f105d51566a0d7110acefb7f19888451156980a1f6172ca44a5b8466"
|
||||
},
|
||||
"tests": {
|
||||
"test/browser/display-native.spec.js": "92897128509ea99f5a0c35f68172d0765095fccd78a8d8a6435cc4bb1c966d77",
|
||||
"test/browser/display-web.spec.js": "154240102bc08dc0d4242807dc3b8fb97c961ae76ca20b6e34d6847094f09805",
|
||||
"test/browser/subtitle-lifecycle.spec.js": "ba91719ccdc9e539aee252d80deaba622fd6e5cb30cdd74697658af1e49dfd58",
|
||||
"test/display-native.test.js": "204f38fff05e2ff6e88d6875e9fe8df30558192450ca7a9685c739c45211ae70",
|
||||
"package.json": "37101b2f2cb1940a8302504df3b0af77686c4c1aeb9920fdc3c31df0b8843149"
|
||||
},
|
||||
"ci": {
|
||||
"passed": 228,
|
||||
"unit": 199,
|
||||
"engineering": 4,
|
||||
"baseline": 25,
|
||||
"strictSourceFiles": 144,
|
||||
"logSha256": "f1b7c8637e9ff114036017794f7bd13700bba5397e65d7a36e14a92399832d87"
|
||||
},
|
||||
"browser": {
|
||||
"stats": {
|
||||
"startTime": "2026-09-11T00:01:36.166Z",
|
||||
"duration": 157683.99099999998,
|
||||
"expected": 693,
|
||||
"skipped": 0,
|
||||
"unexpected": 0,
|
||||
"flaky": 0
|
||||
},
|
||||
"versions": {
|
||||
"chromium": "153.0.8010.12",
|
||||
"firefox": "155.0",
|
||||
"webkit": "26.6"
|
||||
},
|
||||
"counts": {
|
||||
"chromium": 231,
|
||||
"firefox": 231,
|
||||
"webkit": 231
|
||||
},
|
||||
"candidateSha256": "5dc3d53e13486d2b93be4935d37ba3e624a10724bd6b804ed364ab922d9988b4",
|
||||
"nativeCases": [
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "published: native fullscreen belongs to its player and other players cannot exit it",
|
||||
"engine": "chromium",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 1
|
||||
},
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "published: destroyed players stop receiving later native fullscreen events",
|
||||
"engine": "chromium",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 1
|
||||
},
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "published: rejected native fullscreen request removes its temporary change listener",
|
||||
"engine": "chromium",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 2
|
||||
},
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "candidate: native fullscreen belongs to its player and other players cannot exit it",
|
||||
"engine": "chromium",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 1
|
||||
},
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "candidate: destroyed players stop receiving later native fullscreen events",
|
||||
"engine": "chromium",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 1
|
||||
},
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "candidate: rejected native fullscreen request removes its temporary change listener",
|
||||
"engine": "chromium",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 1
|
||||
},
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "candidate: fullscreen assignment rejection shows notice without an unhandled rejection",
|
||||
"engine": "chromium",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 1
|
||||
},
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "candidate: reentrant entry preserves real fullscreen UI and event order",
|
||||
"engine": "chromium",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 1
|
||||
},
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "candidate: pending native entry promise-exit releases a late own entry",
|
||||
"engine": "chromium",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 1
|
||||
},
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "candidate: pending native entry promise-destroy releases a late own entry",
|
||||
"engine": "chromium",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 1
|
||||
},
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "candidate: pending native entry void-exit releases a late own entry",
|
||||
"engine": "chromium",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 1
|
||||
},
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "candidate: pending native entry void-destroy releases a late own entry",
|
||||
"engine": "chromium",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 1
|
||||
},
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "candidate: cancelling an actual native request in the initiating click releases fullscreen",
|
||||
"engine": "chromium",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 1
|
||||
},
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "candidate: rejected exit retains actual fullscreen state and can be retried",
|
||||
"engine": "chromium",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 1
|
||||
},
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "published: native fullscreen belongs to its player and other players cannot exit it",
|
||||
"engine": "firefox",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 0
|
||||
},
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "published: destroyed players stop receiving later native fullscreen events",
|
||||
"engine": "firefox",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 0
|
||||
},
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "published: rejected native fullscreen request removes its temporary change listener",
|
||||
"engine": "firefox",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 0
|
||||
},
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "candidate: native fullscreen belongs to its player and other players cannot exit it",
|
||||
"engine": "firefox",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 0
|
||||
},
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "candidate: destroyed players stop receiving later native fullscreen events",
|
||||
"engine": "firefox",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 0
|
||||
},
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "candidate: rejected native fullscreen request removes its temporary change listener",
|
||||
"engine": "firefox",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 0
|
||||
},
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "candidate: fullscreen assignment rejection shows notice without an unhandled rejection",
|
||||
"engine": "firefox",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 0
|
||||
},
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "candidate: reentrant entry preserves real fullscreen UI and event order",
|
||||
"engine": "firefox",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 0
|
||||
},
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "candidate: pending native entry promise-exit releases a late own entry",
|
||||
"engine": "firefox",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 0
|
||||
},
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "candidate: pending native entry promise-destroy releases a late own entry",
|
||||
"engine": "firefox",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 0
|
||||
},
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "candidate: pending native entry void-exit releases a late own entry",
|
||||
"engine": "firefox",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 0
|
||||
},
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "candidate: pending native entry void-destroy releases a late own entry",
|
||||
"engine": "firefox",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 0
|
||||
},
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "candidate: cancelling an actual native request in the initiating click releases fullscreen",
|
||||
"engine": "firefox",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 0
|
||||
},
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "candidate: rejected exit retains actual fullscreen state and can be retried",
|
||||
"engine": "firefox",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 0
|
||||
},
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "published: native fullscreen belongs to its player and other players cannot exit it",
|
||||
"engine": "webkit",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 0
|
||||
},
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "published: destroyed players stop receiving later native fullscreen events",
|
||||
"engine": "webkit",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 0
|
||||
},
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "published: rejected native fullscreen request removes its temporary change listener",
|
||||
"engine": "webkit",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 0
|
||||
},
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "candidate: native fullscreen belongs to its player and other players cannot exit it",
|
||||
"engine": "webkit",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 0
|
||||
},
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "candidate: destroyed players stop receiving later native fullscreen events",
|
||||
"engine": "webkit",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 0
|
||||
},
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "candidate: rejected native fullscreen request removes its temporary change listener",
|
||||
"engine": "webkit",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 0
|
||||
},
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "candidate: fullscreen assignment rejection shows notice without an unhandled rejection",
|
||||
"engine": "webkit",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 0
|
||||
},
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "candidate: reentrant entry preserves real fullscreen UI and event order",
|
||||
"engine": "webkit",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 0
|
||||
},
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "candidate: pending native entry promise-exit releases a late own entry",
|
||||
"engine": "webkit",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 0
|
||||
},
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "candidate: pending native entry promise-destroy releases a late own entry",
|
||||
"engine": "webkit",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 0
|
||||
},
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "candidate: pending native entry void-exit releases a late own entry",
|
||||
"engine": "webkit",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 0
|
||||
},
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "candidate: pending native entry void-destroy releases a late own entry",
|
||||
"engine": "webkit",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 0
|
||||
},
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "candidate: cancelling an actual native request in the initiating click releases fullscreen",
|
||||
"engine": "webkit",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 0
|
||||
},
|
||||
{
|
||||
"file": "display-native.spec.js",
|
||||
"title": "candidate: rejected exit retains actual fullscreen state and can be retried",
|
||||
"engine": "webkit",
|
||||
"status": "passed",
|
||||
"unhandledErrors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequestCount": 0
|
||||
}
|
||||
],
|
||||
"reportSha256": "49120c1311187454412037e21b398b4d1a6acf40ed6b774e17738f3c92dbe6aa"
|
||||
},
|
||||
"baseline": {
|
||||
"stats": {
|
||||
"startTime": "2026-09-10T23:43:10.375Z",
|
||||
"duration": 12331.226,
|
||||
"expected": 9,
|
||||
"skipped": 0,
|
||||
"unexpected": 9,
|
||||
"flaky": 0
|
||||
},
|
||||
"sha256": "ba10fe9ecdffc1ad0414e816d39e8cb900511914c5b67430d8849fbcae536b67"
|
||||
},
|
||||
"earlierRuns": [
|
||||
{
|
||||
"name": "core16-native-first.json",
|
||||
"stats": {
|
||||
"startTime": "2026-09-10T23:51:35.856Z",
|
||||
"duration": 23404.386000000002,
|
||||
"expected": 69,
|
||||
"skipped": 0,
|
||||
"unexpected": 0,
|
||||
"flaky": 0
|
||||
},
|
||||
"sha256": "95d9aa620d5043684b3ae195116c147267353b6cf0f7317a44c72203b0d005a5"
|
||||
},
|
||||
{
|
||||
"name": "core16-native-expanded.json",
|
||||
"stats": {
|
||||
"startTime": "2026-09-10T23:58:16.370Z",
|
||||
"duration": 46784.162,
|
||||
"expected": 159,
|
||||
"skipped": 0,
|
||||
"unexpected": 3,
|
||||
"flaky": 0
|
||||
},
|
||||
"sha256": "64d7354e98e60fefc54694cbe1b3a466095e8388d6239fbd3ef7c040bf58fba5"
|
||||
},
|
||||
{
|
||||
"name": "core16-native-recovery.json",
|
||||
"stats": {
|
||||
"startTime": "2026-09-11T00:00:21.333Z",
|
||||
"duration": 47202.782,
|
||||
"expected": 174,
|
||||
"skipped": 0,
|
||||
"unexpected": 0,
|
||||
"flaky": 0
|
||||
},
|
||||
"sha256": "2d0b3dc290c3fafc93e8533f51bf4684785e76b27451d1206172c9bf372485f6"
|
||||
}
|
||||
],
|
||||
"build": {
|
||||
"logSha256": "133fcdd21d6ba4597663fd1c9f2d0e7be8fcf0be8a5b7faf7cb66834db226397",
|
||||
"i18nLogSha256": "404c25d0f1d30e4f70cf951411d2197651a616a5b486b80582fa0ae41e591efe"
|
||||
},
|
||||
"remaining": [
|
||||
"WebKit video-only fallback, PiP, mini, sizing and orientation migration",
|
||||
"Mode composition, destroy/exit failure boundaries and physical-device evidence",
|
||||
"Full final installed UMD/legacy and consumer acceptance; risks remain open; CORE-16 has no completed-task commit"
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,144 @@
|
||||
# CORE-16 显示模式迁移
|
||||
|
||||
状态:完成。以下阶段记录保留当时的进度;当前交付结论以末尾最终验收为准。
|
||||
|
||||
## 第一批:页面全屏和位置快照
|
||||
|
||||
fullscreenWebMix.ts 保留旧 boolean 属性及真实 DOM class getter,委托 display/web-fullscreen.ts 管理样式、位置快照和操作代次。display/placement.ts 保存原父节点与后继节点,display/types.ts 声明最小宿主。实际核心入口仍然是 JS,不把单个模式迁移计作全部显示服务完成。
|
||||
|
||||
浏览器先对发布包与迁移前候选复现:重复进入覆盖原始样式,空样式退出后残留 100% 宽高;body 模式丢失原父节点/兄弟顺序,静态 flag 中途改变会影响返回位置;destroy 不恢复 body 中的播放器,留下可见孤立节点。36 项基线中发布包 18 项确认缺陷,候选 18 项按目标行为失败,原始报告保留。
|
||||
|
||||
修正策略:
|
||||
|
||||
- 一次进入会话只捕获一次样式和位置。重复 true 仍保留原 true 事件,不再覆盖快照。
|
||||
- false 恢复实际原父节点和后继,不依赖退出时的 FULLSCREEN_WEB_IN_BODY 值;后继已移除则追加到原父节点。
|
||||
- 空 style 与未设置 style 分别按原属性恢复。resize 可继续更新控件自身的 CSS 变量,不覆盖用户宽/高/颜色。
|
||||
- 实例销毁先恢复自己移出的播放器,再由现有 template.destroy 执行 removeHtml;资源清理不额外发 fullscreenWeb/resize 事件。
|
||||
- 每次 setter 都有代次;事件监听者退出/重新进入/销毁后,旧操作不再写入或发出过期 resize。
|
||||
- 进入失败尝试恢复原布局;退出移动失败保留快照,可由下一次 false 重试。用户 DOM/事件回调的任意外部副作用不属于通用事务回滚。
|
||||
|
||||
兼容边界:普通调用仍同步、事件顺序为 fullscreenWeb 再 resize,class/getter/静态 flag 名称不变。state 仍通过旧门面退出其他模式;本批未解决那些模式自己的异步退出和权限问题。公开声明未修改。
|
||||
|
||||
## 阶段验证与限制
|
||||
|
||||
- 已新增真实浏览器的新旧恢复契约和候选重入/失败用例。
|
||||
- 首轮候选样式断言误把 CORE-13 的 --art-controls-height 更新计作恢复失败;原始报告保留。改为退出事件内验证精确快照、resize 后验证宽高颜色,并附带完整前后样式。
|
||||
- 本批 CI 210 项、139 个生产 TS 文件严格检查、完整工作区三浏览器 651 项通过,其中页面全屏 51 项。无重试或跳过;四个 TS 源文件涉及页面全屏门面、宿主、位置与恢复逻辑。见 [阶段证据](../baselines/display-web-validation.json)。
|
||||
- 重新用同一浏览器构建配置生成候选,核对 SHA-256 与全部 651 项的 manifest 一致;核心三个分发格式和 docs/compiled 字节一致,构建与语言生成通过。本批尚未验证安装 UMD/legacy,不把工作区验证说成发布验收。
|
||||
- 原生全屏/PiP 权限、mini 拖拽、方向锁与退出失败、完整安装 UMD/legacy 都仍未完成 CORE-16 验收。后续还要验证旋转监听者与用户原始宽高恢复的组合,以及销毁期间位置恢复本身失败的处理;两个新增风险保持 open。
|
||||
- 无新依赖或脚本;继续 Node 24.21.0 / Yarn 1.22.22。完成 CORE-16 后才作对应独立本地提交,不推送或发布。
|
||||
|
||||
## 第二批:原生 document 全屏
|
||||
|
||||
fullscreenMix.ts 保留首个 metadata 后安装属性的时机,调用 display/fullscreen-adapter.ts、native-fullscreen.ts、fullscreen-request.ts;原 vendor screenfull 内容和前缀映射不变。新增最小 FullscreenHost/NativeFullscreenAdapter 类型,公开声明不变。
|
||||
|
||||
发布包在三个引擎均复现:一个实例进入会使另一实例报告全屏并可退出别人的全屏;destroy(false) 后仍会收到后续全屏事件和 class 更新;请求拒绝后遗留临时 change 监听。登记 BASE-DOM-10、BASE-LIFE-18,保持 open 到最终安装产物验收。
|
||||
|
||||
- getter 与退出按本实例 player/video 的真实归属判断;其他实例的事件不再污染本实例。普通 fullscreen → state/class → resize 顺序保留。
|
||||
- 请求在调用栈中同步发起,不提前 await,保留用户手势权限。原生 Promise 拒绝保留错误对象;void 前缀 API 使用 change/error 结算。
|
||||
- 每个请求的临时监听和实例永久监听分别随操作/实例 scope 清理。取消及时结算;现代 Promise 的晚到进入及销毁均作归属检查后退出。普通属性赋值提供 notice 和内部 rejection handler,直接调用 descriptor setter 仍能 await/catch 原错误。
|
||||
- 无法取消且不返回 Promise 的旧 API 使用一个按 document 共享的晚到事件 guard。只有该取消路径才创建它;弱键保存目标节点,值只持有 native exit,不引用 art/scope。新请求清除旧标记,其他节点不被退出。此监听有意保留到 document 结束,不计为实例监听器泄漏。
|
||||
- 补测修复了本批初版的重入 true 丢失 class/resize、fullscreenerror 抢先覆盖原始 Promise 错误,以及取消后 change 先于 Promise 完成导致短暂旧状态的问题。
|
||||
|
||||
验证过程:初版新旧原生/页面全屏 69 项通过;新增组合首轮 159 通过、3 失败,原因是 notice 测试未限定主实例而匹配两个元素,已改为明确主容器定位,原始报告保留。随后原生/页面全屏/字幕 174 项三引擎通过;最后一处晚到 change 修正后,完整工作区三浏览器 693 项通过,其中原生全屏 42 项、页面全屏 51 项。新增 18 项 Node 测试已接入原有 test:unit/CI,合计 228 项通过;144 个生产 TS 文件及五组消费者严格检查通过。没有新依赖。
|
||||
|
||||
核心三个格式与语言重新生成,docs/compiled 与核心分发产物字节相同。独立用浏览器配置重建候选,SHA-256 为 5dc3d53e13486d2b93be4935d37ba3e624a10724bd6b804ed364ab922d9988b4,与 693 项报告中全部 manifest 一致。详见 [阶段证据](../baselines/display-native-validation.json);该证据明确标记 partial,未运行最终安装 UMD/legacy 验收。
|
||||
|
||||
限制:桌面实际全屏/用户手势与受控 Promise/void 故障注入分别记录。受控 void API 不等于旧 Safari/iOS 实机验证;video-only WebKit fallback 目前只保留旧实现并清理订阅,尚未完成语义修正。销毁后的 native exit 为 best effort,浏览器拒绝仍可能保留原生全屏;普通失败退出保留实际 getter/class 并允许重试。最终模式组合与安装 UMD/legacy 尚未验收。
|
||||
|
||||
## 第三批:video-only WebKit 全屏
|
||||
|
||||
fullscreenMix.ts 的 fallback 委托 display/video-fullscreen.ts;display/video-fullscreen-state.ts 管理实际状态判定和无实例引用的晚到取消。依照 [Apple 的视频全屏事件说明](https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/ControllingMediaWithJavaScript/ControllingMediaWithJavaScript.html),补充 webkitbeginfullscreen / webkitendfullscreen 及 presentation-mode 监听,并保留 document:webkitfullscreenchange 的历史桥接。
|
||||
|
||||
受控浏览器基线确认发布包在视频自身显示全屏时 getter 仍为 false,原生进入/退出事件不更新 ArtPlayer;销毁时视频保持全屏。登记 BASE-DOM-11、BASE-LIFE-19。浏览器 fixture 明确关闭 document 全屏能力并模拟视频方法/状态,不能把这项复现解释为已在 iPhone 上复现。首轮 fixture 未给原型方法设置 writable,影响新增失败注入场景;保留报告,修正后才用于验证这些场景。前两组新旧状态/销毁基线不依赖方法覆写,仍可使用。
|
||||
|
||||
普通 setter 仍同步、返回 undefined;原生同步异常保持同一对象并允许下一次调用重试。新状态读取优先区分 fullscreen 与 picture-in-picture;没有 presentation 属性时使用 webkitDisplayingFullscreen,再回退到 document/原生事件。多种原生通知只发一次相同状态变化,事件内重入/销毁停止旧 resize。
|
||||
|
||||
实例销毁时释放所有实例监听,退出当前视频全屏。不能取消的 pending void 进入只保留视频自身的静态监听与 WeakSet 标记;回调不引用 art/scope,新进入会移除旧取消标记。正常销毁退出的同步错误由现有生命周期清理收集器传播;晚到取消退出是 best effort。该路径不添加 document 全局监听。
|
||||
|
||||
新增 9 项 Node 测试已接入 test:unit,包括只有事件而没有状态属性的 shim、PiP 状态区分、事件顺序、重入、失败与晚到清理;没有新依赖。首轮 video-only 与 document 全屏 60 项三引擎通过。最终 CI 237 项、146 个生产 TS 文件和五组消费者严格检查通过;当前源码的 video-only/document/web 全屏与字幕组合 192 项三引擎通过,其中 fallback 18 项。该次是相关组合回归,没有重跑完整浏览器套件。
|
||||
|
||||
核心三个格式与语言生成通过,docs/compiled 与分发字节相同;独立重建候选并核对全部报告 manifest。详见 [video-only 阶段证据](../baselines/display-video-validation.json),仍标记 partial,物理设备与最终安装产物门槛保持开放。
|
||||
|
||||
PiP 下一步入口审查发现:原生分支 getter 返回 document.pictureInPictureElement(Element/null),WebKit 分支返回 boolean,而公开声明统一为 boolean。迁移时必须保留旧合法 JS 返回形态,同时处理跨实例归属和公开类型协调;不能直接把所有返回强制转为 boolean 来掩盖差异。该点目前是源码观察,下一批补发布基线与消费者用例。
|
||||
|
||||
## 第四批:PiP
|
||||
|
||||
pipMix.ts 选择可调用的原生或 WebKit 能力;native-pip.ts 与 webkit-pip.ts 分别拥有其状态、事件和销毁。发布包复现确认:原生 getter 返回 Element/null,setter 返回 undefined;WebKit getter 为 boolean,重复 setter 保持同步 pip 通知。本批保留这些形态,内部 PipProperty 使用真实联合类型,不修改历史公开 boolean 声明;差异登记 BASE-TYPE-10 并由 CORE-21 继续协调。
|
||||
|
||||
原生分支按本视频归属读取/退出,避免关掉别人的 PiP;销毁时先移除实例监听,再退出自己持有的窗口。进入调用不 await,保留手势调用栈。异步拒绝显示原错误 notice,旧操作不覆盖新 notice;由于原 setter 返回 void,内部处理拒绝以避免无消费者的 unhandledrejection,没有伪造一个新 Promise 返回。同步原生错误仍同步抛出。取消或销毁后的晚到进入会检查归属后退出。
|
||||
|
||||
WebKit 分支保留初始 inline 设置和重复 setter 事件;补充原生 UI 的 presentation/PiP 事件,避免多种原生通知造成重复事件。进入时检查当前能力,媒体就绪后可以重试;false 不关闭另一种 fullscreen presentation。pending void 进入取消只保留视频自身的静态 guard 与弱标记,不引用 art/scope。同步销毁失败与晚到清理限制沿用显示生命周期规则。
|
||||
|
||||
基线 27 项中,旧发布的 9 项和候选普通契约 3 项通过,其余候选 15 项按新目标失败,包含跨视频归属、销毁、晚到进入和未处理拒绝。迁移后首轮 native PiP/video-only 45 项通过;新增 WebKit 组合后 162 项通过。补测修复了原生进入在 mode 同步中被取消时发出未曾对应 true 的 false 事件;WebKit 的显式 false setter 仍保留原 false 通知。14 项 Node 测试已接入 test:unit;无新依赖。
|
||||
|
||||
实际原生验证:当前 Chromium、Firefox 均支持标准 API,成功获取非零大小 PiP 窗口、记录视频播放进度,并验证向页面全屏/原生全屏切换;当前 WebKit 标准 API 不可用,记录能力边界。受控 WebKit 用例另行验证 fallback,不将其算成原生 PiP 或物理 iOS 通过。
|
||||
|
||||
最终本批 CI 251 项、149 个生产 TS 文件及五组消费者严格检查通过;PiP/全屏组合 162 项三浏览器通过,其中 PiP 51 项。核心三个分发格式和语言生成通过,docs/compiled 字节一致;独立重建候选并逐个核对浏览器 manifest。见 [PiP 阶段证据](../baselines/display-pip-validation.json),其中单独保存实际 PiP 能力/窗口/播放证据,明确标记 partial;没有进行最终安装 UMD/legacy 验收。
|
||||
|
||||
## 第五批:mini
|
||||
|
||||
miniMix.ts 保留 boolean 门面;display/mini.ts 管理视频位置和操作代次,mini-view.ts 管理浮窗/播放按钮/订阅,mini-drag.ts 管理拖拽与 document 订阅,mini-layout.ts 为纯位置计算。五个新 TS 文件按职责分拆,共享此前的 Placement 工具,没有新依赖。
|
||||
|
||||
新旧浏览器复现确认:旧退出将视频直接 prepend 到 player,丢失原父节点和后继;两种 destroy 都留下 body 浮窗。登记 BASE-DOM-13、BASE-LIFE-21,保持 open。最初隐藏拖拽测试只检查形状而未比较持久值,已增强为前后存储完全相同,不将最初该条通过视为清理证据。
|
||||
|
||||
- 每轮首次进入记录视频实际 parent/next;重复进入不覆盖。退出失败保留快照,可重试;恢复前先分离旧快照,避免 DOM 回调重入时抹掉新会话的位置。
|
||||
- 自建浮窗、播放与拖拽订阅均由实例 scope 清理;隐藏停止拖拽、保留可复用浮窗,销毁恢复视频并移除自建节点。调用方预置的 $mini 不删除,销毁恢复其初始 display。
|
||||
- 保留首次 video-first/default display、重用 video-last/flex 的旧 DOM/样式细节,保留重复 setter 的 mini 事件和 top/left 存储键。
|
||||
- 拖拽使用固定定位对应的 client 坐标;结束时限制在视口内。普通默认仍为 50px 边距,非有限/越界存储重置,小视口通过 max-width/max-height 保持可见。
|
||||
- 自定义图标 getter 可能隐藏/销毁/再次进入 mini。重复创建的受控复现最初在三个引擎失败,已将创建串行化为单个浮窗,并验证未完成视图的清理和失败后重试。
|
||||
|
||||
纯位置计算 4 项 Node 测试接入 test:unit;新增内部 boolean/数字坐标类型用例。首轮 21 项、扩展 51 项通过;修正创建重入后 219 项 mini/PiP/全屏组合通过。补充销毁后的拖拽订阅验证后,最终组合 222 项通过,其中 mini 60 项;CI 255 项、154 个生产 TS 文件和五组消费者严格检查通过。初版 Cleanup 回调的 void/undefined 类型差异已修正,保留失败日志。
|
||||
|
||||
核心三个分发格式及语言生成通过,docs/compiled 字节一致;重新生成候选并逐个核对 222 项 manifest。见 [mini 阶段证据](../baselines/display-mini-validation.json)。本批没有重跑完整浏览器套件或最终安装 UMD/legacy;mobile touch 拖拽和物理设备验证未计作通过。
|
||||
|
||||
下一步处理尺寸/方向组合及剩余失败退出边界。回退时恢复对应旧 mixin,删除 display 新模块及类型用例,并由构建重新生成产物;保留已复现的基线记录。
|
||||
|
||||
## 第六批:尺寸、宽高比与翻转
|
||||
|
||||
autoSizeMix、autoHeightMix、aspectRatioMix、flipMix 迁移 TS,display/sizing.ts 集中纯尺寸计算,sizing-types.ts 限定各门面的最小依赖。保留自己的不可枚举/不可配置属性、可抽取方法、同步 undefined 返回、正常布局和事件;宽高比仍用原 split/Number 转换,保留多段字符串的旧解析、dataset、notice 与重复事件;翻转仍允许自定义字符串和 falsy 默认值。没有新依赖或公开声明变更。
|
||||
|
||||
发布基线 18 项中 15 通过、候选目标 3 失败,复现媒体尺寸为 0 时旧 autoSize 局部覆盖宽度、autoHeight 发出 NaN。新增 BASE-DOM-14,候选对未就绪/隐藏/非有限几何不改布局、不发无效测量,尺寸恢复后正常应用。非法比例仍保存输入并发原事件/提示,CSS 不再局部改写;这属于异常输入的布局修正。销毁后的调用不再改写残留节点。
|
||||
|
||||
4 项纯计算测试覆盖比例保持、容器边界、尺寸未就绪后的恢复、非有限值和浮点溢出/下溢。浏览器尺寸用例 33 项覆盖发布/候选有效比例、属性描述符、可抽取方法、解析、隐藏后恢复、非法输入和销毁。扩展首轮 30 通过、3 失败来自测试错误地把 notice.show 的 boolean getter 当字符串;已改为检查真实提示节点,保留该失败日志。
|
||||
|
||||
最终本批 CI 259 项(230 单元、4 工程、25 基线)、160 个生产 TS 文件及五组消费者严格检查通过。尺寸/mini/PiP/全屏组合 255 项三浏览器通过,其中尺寸 33 项;核心三个格式和 i18n 构建通过,docs/compiled 字节一致。重新构建的候选指纹逐一匹配 255 项 manifest,见 [尺寸阶段证据](../baselines/display-sizing-validation.json)。这不是全量浏览器或最终安装包验证。
|
||||
|
||||
CORE-16 仍在实施:自动方向、旋转样式组合、恢复失败以及最终安装 UMD/legacy 验收尚未结束,不作完成任务提交。回退本批时恢复四个 JS 门面、删除 sizing 模块/类型/测试并用仓库脚本重建,保留基线缺陷记录。
|
||||
|
||||
## 第七批:自动方向
|
||||
|
||||
autoOrientation.ts 保留内置插件名称、只读 state、移动端启用条件和横竖不匹配规则;拆出 orientation-web.ts、orientation-native.ts、orientation-types.ts。原泛型插件注册器不声明完整 JS 构造宿主,因此仅在内置装配处桥接工厂类型;实际实现使用最小 OrientationHost,完整主入口类型仍由 CORE-21 接续。无新依赖、无公开声明变化。
|
||||
|
||||
基线 12 项中旧发布 6 项通过、候选目标 6 项失败,复现退出旋转会清空调用方样式、方向 lock 晚到后会重新添加 active class 且未 unlock。分别登记 BASE-DOM-15 和 BASE-LIFE-22。实现由实例 scope 拥有订阅与 timer,退出/销毁取消旧会话延时;重复全屏重新应用旋转尺寸但不覆盖首次快照。fullscreenWeb 退出先恢复完整样式,旋转 false 不再清空;销毁时旋转清理先于 fullscreenWeb 最终恢复。
|
||||
|
||||
原生方向锁立即调用,成功后只有当前活跃请求能标记 class;退出/销毁立即尝试 unlock,晚到成功再次释放。弱平台对象 token 限定请求归属,旧实例不能 unlock 新实例的锁;旧错误不覆盖新 notice,当前错误保留身份并允许重试。锁成功前后的取消均有单元用例。外部代码直接调用方向 API 无法被 ArtPlayer 自动追踪;平台拒绝 unlock 仍按旧行为 best effort。标准依据为 [Screen Orientation](https://www.w3.org/TR/screen-orientation/),不将受控 API 回调当作物理设备能力证明。
|
||||
|
||||
8 项 Node 测试接入 test:unit。首次浏览器迁移 9 通过、3 失败:源码已恢复用户四个属性,但 resize 合法添加 --art-controls-height,原测试错误地比较整个 style。诊断保存了实际前后字符串;改为逐一比较受管理属性,destroy 仍检查完整快照。扩展轮 17 通过、7 失败,其中重复进入覆盖旋转尺寸是实际缺陷,已修复;计时器测试在 Firefox 的时钟继续流逝,已用 pauseAt 明确冻结后按时间推进。修正后 24 项通过,再加入 pending destroy 与真实旋转播放/比例/翻转组合。
|
||||
|
||||
最终方向 30 项、全部当前显示模块组合 285 项三浏览器通过;CI 267 项(238 单元、4 工程、25 基线)、164 个生产 TS 文件及五组消费者严格检查通过。核心三个格式和 i18n 构建通过,docs/compiled 一致,独立重建指纹与全部 285 项 manifest 匹配,见 [方向阶段证据](../baselines/display-orientation-validation.json)。桌面引擎模拟移动 UA,方向 lock 为受控接口;真实媒体播放已验证,但物理旋转、安全区域和设备方向限制仍需设备证据。
|
||||
|
||||
CORE-16 保持 doing;下一步处理页面全屏销毁时位置恢复失败等边界,再跑完整源码/实际安装 UMD/legacy 和消费者出口。回退恢复 autoOrientation.js、内置装配与对应生成物;保留已复现缺陷和日志。
|
||||
|
||||
## 第八批:剩余退出与恢复失败边界
|
||||
|
||||
页面全屏销毁恢复测试先跑 12 项,6 项通过、6 项失败,确认 insertBefore 在移动前抛错时两种 destroy 都会留下 body 中的播放器。现在销毁恢复失败后,只有仍在原归属之外的播放器才尝试移除,防止孤立节点;已归位再抛错则交给正常 template.destroy。原错误继续抛出,二次移除也失败时由生命周期错误集合保留两者。异常 fallback 可能使 destroy(false) 的残留节点变成脱离文档状态,这比保留错误位置的可见播放器更可控,且调用者仍收到原恢复失败;正常退出的可重试快照不变。
|
||||
|
||||
原生 PiP 补充过期 leave 回调复现:视频仍属于当前 PiP 时,旧逻辑错误发出 false;现在以当前归属过滤该信号,真正退出仍发 false。15 项 PiP 单元测试通过。该补充属于 BASE-DOM-12 的事件一致性,未新增公开 API。
|
||||
|
||||
原生 void 全屏退出补充复现:文档实际全屏的是本实例 video、原请求 target 是 player 时,旧等待条件提前结算。现在进入仍等待 target,退出由实例归属判断,直到 player/video 都不再拥有全屏;另一元素成为全屏也可完成本次退出,不会把它当作自己。19 项原生全屏单元测试通过,并加入实际分发可运行的三浏览器受控用例。该补充属于 BASE-LIFE-18 的结算边界。
|
||||
|
||||
正在进行最后的源码与安装包出口验证;前七批 manifest 仍代表各自历史源码,不用它们覆盖本批新增改动。
|
||||
|
||||
## 最终验收
|
||||
|
||||
- 全屏、页面全屏、video-only fallback、PiP、mini、自动尺寸、宽高比、翻转与自动方向已迁移 TS,分离能力适配、状态、几何、DOM 和资源作用域。核心 159 个与 chapter 5 个生产 TS 文件严格检查通过;公开 mixin 属性描述符、旧返回形态及正常事件/DOM 契约保持。
|
||||
- yarn ci:check:269 项通过(240 单元、4 工程、25 基线)。此前源码全量 897 项通过,但它早于最后的 void video-owner 修正;最终安装包矩阵包含新用例,不把旧源码指纹冒充最终源码。
|
||||
- 实际 core/chapter tarball 仓库外安装:27 项运行时、五组类型零诊断。最终安装 UMD 和 legacy 各 900 项三浏览器通过,其中各 300 项显示模块检查;无重试、跳过或未处理页面错误。
|
||||
- [最终证据](../baselines/display-validation.json)核对当前全部核心源码和构建快照、三个分发格式与 tarball/docs、全部包内文件、公开类型、架构说明、22 个语言文件和独立分发许可。
|
||||
- Chromium/Firefox 真实原生 PiP 窗口与播放推进通过;三引擎真实桌面 document fullscreen 通过。当前 WebKit 标准 PiP 不可用,video-only/void/WebKit PiP/方向锁为受控路径,物理 iOS/Android、安全区域、实际方向/手势等仍交 REL-03/REVIEW-02,不声称设备或发布准入已通过。
|
||||
- 销毁恢复失败的异常降级为移除仍脱离归属的播放器并保留原错误;原生平台拒绝退出或 unlock 时只能 best effort。外部任意 DOM/方向 API 副作用不被承诺可回滚。
|
||||
- 无新增依赖、锁文件或版本修改。公开 PiP 历史 boolean 声明差异 BASE-TYPE-10 继续由 CORE-21 协调;不将保持旧 JS Element/null 返回改为强制 boolean。
|
||||
- 回退本任务时恢复对应旧门面、autoOrientation 及内置装配,移除 display 新模块/对应测试,用仓库脚本重新构建核心和 i18n;保留历史基线与缺陷记录。
|
||||
|
||||
关闭本任务十三个 DOM/生命周期缺陷。当前 214 项中 52 完成、162 待办;下一项 CORE-17 键盘、手势、焦点和全局事件。全生态、设备、远端 CI、多轮复盘和发布门槛继续按计划推进。本任务独立本地提交,不推送或发布。
|
||||
+3
-2
@@ -4,7 +4,7 @@
|
||||
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 214 项,范围 22 个包及工作区/示例。
|
||||
|
||||
状态:todo 163 / doing 0 / blocked 0 / done 51 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
状态:todo 162 / doing 0 / blocked 0 / done 52 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
|
||||
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
| CORE-13 | artplayer<br>迁移组件注册与控件 | CORE-12, CORE-08 | Component/control/layer/contextmenu 的资源与 TS 边界 | add/update/remove、name、selector、mounted/beforeUnmount 通过;处理 BASE-DOM-02 窄容器控件裁切,保留 controls.update 的旧 undefined 返回值 | H | done |
|
||||
| CORE-14 | artplayer<br>拆分设置树、渲染和布局 | CORE-13 | setting model/selection/render/layout 的分批实现 | 不强行统一控件返回值,嵌套选择、默认高亮、更新清理通过;结合 BASE-DOM-02 验证窄容器设置定位;接续 BASE-TYPE-07 的 find null、add/update item、remove undefined 声明冲突 | H | done |
|
||||
| CORE-15 | artplayer<br>迁移字幕获取、解析和渲染 | CORE-11, CORE-13 | subtitle 请求/track/DOM/URL 生命周期与类型 | 过期结果、偏移、cue 数组、原生全屏及错误路径通过;接续 BASE-TYPE-07 的真实 cue 数组与旧隐式 scalar 回调声明兼容 | H | done |
|
||||
| CORE-16 | artplayer<br>迁移全屏、PiP、mini 与尺寸模式 | CORE-11, CORE-12 | 显示模式服务和原公开属性门面 | 模式互斥/恢复位置、手势权限、移动方向和退出失败通过 | H | todo |
|
||||
| CORE-16 | artplayer<br>迁移全屏、PiP、mini 与尺寸模式 | CORE-11, CORE-12 | 显示模式服务和原公开属性门面 | 模式互斥/恢复位置、手势权限、移动方向和退出失败通过 | H | done |
|
||||
| CORE-17 | artplayer<br>迁移键盘、手势、焦点和全局事件 | CORE-13, CORE-16 | events/hotkey 的明确 document 与资源归属 | 跨 document 重绑、触摸/键盘、快捷键及销毁无重复响应;修复 BASE-PERF-01 pending resize debounce 销毁后执行,并验证正常 resize 不变 | H | todo |
|
||||
| CORE-18 | artplayer<br>迁移内置插件和提示模块 | CORE-14, CORE-16, CORE-17 | autoPlayback/autoOrientation/fastForward/lock/miniProgressBar 及 info/notice/loading/mask | 默认启用条件、storage 格式、定时器/动画清理通过;结合 BASE-PERF-01 防止销毁后重新安排 notice timer;接续 BASE-TYPE-07 的 notice.show boolean getter 与历史读类型冲突 | M | todo |
|
||||
| CORE-19 | artplayer<br>整理进度、质量、缩略图和截图 | CORE-10, CORE-13, CORE-16 | 相关 player/control 功能与纯计算分离 | 截图跨域失败、缩略图布局、quality/进度边界保持 | M | todo |
|
||||
@@ -450,6 +450,7 @@
|
||||
- CORE-13: [记录](changes/2026-09-11-CORE-13-components.md) [记录](baselines/components-validation.json)
|
||||
- CORE-14: [记录](changes/2026-09-11-CORE-14-setting.md) [记录](baselines/setting-validation.json)
|
||||
- CORE-15: [记录](changes/2026-09-11-CORE-15-subtitle.md) [记录](baselines/subtitle-validation.json)
|
||||
- CORE-16: [记录](changes/2026-09-11-CORE-16-display.md) [记录](baselines/display-validation.json)
|
||||
- PKG-CHAPTER-01: [记录](changes/2026-09-10-PKG-CHAPTER-01-contract.md) [记录](baselines/chapter-contract.md)
|
||||
- PKG-CHAPTER-02: [记录](changes/2026-09-10-PKG-CHAPTER-02-tests.md) [记录](baselines/chapter-validation.json)
|
||||
- PKG-CHAPTER-03: [记录](changes/2026-09-10-PKG-CHAPTER-03-typescript-modules.md) [记录](baselines/chapter-migration-validation.json)
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# 进度与证据
|
||||
|
||||
## 当前实施:CORE-16 显示模式完成
|
||||
|
||||
全屏、video-only fallback、PiP、mini、自动尺寸、宽高比、翻转和方向已迁移 TS,拆清能力、状态、几何、DOM 和资源归属。修复重入/取消/销毁、节点和样式恢复、跨实例归属、过期事件及无效尺寸;正常旧 API、返回和事件保持。
|
||||
|
||||
CI 269 项、核心 159 个与 chapter 5 个生产 TS 文件严格检查通过。实际安装 UMD 和 legacy 各 900 项三浏览器通过(各含 300 项显示检查);真实 tarball 27 项运行时及五组类型零诊断。源码、产物、全部包内文件、公开类型、语言与许可指纹一致。见 [交付](changes/2026-09-11-CORE-16-display.md) 和 [最终证据](baselines/display-validation.json)。
|
||||
|
||||
关闭十三个 DOM/生命周期缺陷。BASE-TYPE-10 公开 PiP 声明继续交 CORE-21;物理设备与受控 fallback 的限制仍保留在 REL-03/REVIEW-02。当前 214 项:52 完成、162 待办;下一项 CORE-17 键盘、手势、焦点与全局事件。本任务独立本地提交,不推送或发布。
|
||||
|
||||
## 当前实施:CORE-15 字幕请求、track 与渲染完成
|
||||
|
||||
字幕管理器拆为 7 个 TS 模块,字幕偏移 mixin 同时迁移;请求取消及时结算,过期结果停止写入;仅释放自建 URL,处理 track 重入/失败恢复、原生 cue/全屏、错误及清理。正常转换、回调、DOM、返回和事件契约保留。
|
||||
|
||||
@@ -70,3 +70,17 @@
|
||||
| BASE-DOM-08 | resolved / 已复现 | Sharing a live setting object leaves a second published instance blank and can steal candidate bindings | CORE-14 |
|
||||
| BASE-LIFE-15 | resolved / 已复现 | Subtitle requests overwrite newer state and mutate tracks after destroy | CORE-15 |
|
||||
| BASE-LIFE-16 | resolved / 已复现 | Subtitle URLs revoke caller resources and leak the final generated resource | CORE-15 |
|
||||
| BASE-DOM-09 | resolved / 已复现 | Web fullscreen loses style and placement across repeated entry or configuration changes | CORE-16 |
|
||||
| BASE-LIFE-17 | resolved / 已复现 | Destroy in body web fullscreen leaves a connected orphan player | CORE-16 |
|
||||
| BASE-DOM-10 | resolved / 已复现 | Native fullscreen state and exit affect unrelated player instances | CORE-16 |
|
||||
| BASE-LIFE-18 | resolved / 已复现 | Native fullscreen retains destroyed listeners and leaks rejected request listeners | CORE-16 |
|
||||
| BASE-DOM-11 | resolved / 已复现 | Video-only fullscreen ignores native video state and begin/end transitions | CORE-16 |
|
||||
| BASE-LIFE-19 | resolved / 已复现 | Video-only fullscreen survives player destruction and late entry | CORE-16 |
|
||||
| BASE-DOM-12 | resolved / 已复现 | PiP reports another video as active and WebKit ignores native presentation transitions | CORE-16 |
|
||||
| BASE-LIFE-20 | resolved / 已复现 | PiP survives destruction or late entry and rejected assignments become unhandled | CORE-16 |
|
||||
| BASE-TYPE-10 | open / 已复现 | PiP getter is declared boolean but native runtime returns an element or null | CORE-16, CORE-21 |
|
||||
| BASE-DOM-13 | resolved / 已复现 | Mini exit loses the video's original parent and sibling position | CORE-16 |
|
||||
| BASE-LIFE-21 | resolved / 已复现 | Mini leaves a connected popup after player destruction | CORE-16 |
|
||||
| BASE-DOM-14 | resolved / 已复现 | Unready media sizing partially overwrites layout and emits NaN height | CORE-16 |
|
||||
| BASE-DOM-15 | resolved / 已复现 | Auto orientation exit clears caller-owned inline dimensions and transforms | CORE-16 |
|
||||
| BASE-LIFE-22 | resolved / 已复现 | Orientation lock success after fullscreen exit reactivates a cancelled player | CORE-16 |
|
||||
@@ -1350,6 +1350,309 @@
|
||||
"test/subtitle.test.js"
|
||||
],
|
||||
"resolutionRationale": "Final installed UMD and legacy each pass 600 real-browser checks. Subtitle cancellation settles promptly and stale work cannot commit. Only generated URLs are owned; native fullscreen-kind rebuilds retain them and failure/reentry/destroy cleanup is verified. Runtime and type consumers pass against the exact packed source and artifacts."
|
||||
},
|
||||
{
|
||||
"id": "BASE-DOM-09",
|
||||
"title": "Web fullscreen loses style and placement across repeated entry or configuration changes",
|
||||
"closureCriteria": "Final installed matrices prove original styles, parent and sibling restoration, resize integration, reentry and failure recovery.",
|
||||
"confirmation": "reproduced",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"CORE-16"
|
||||
],
|
||||
"evidence": [
|
||||
"test/browser/display-web.spec.js",
|
||||
"refactor/changes/2026-09-11-CORE-16-display.md"
|
||||
],
|
||||
"compatibleResolution": "Capture the first entry layout, restore original ownership on exit/destroy, preserve ordinary events, and stop obsolete operations after reentry.",
|
||||
"resolutionEvidence": [
|
||||
"refactor/changes/2026-09-11-CORE-16-display.md",
|
||||
"refactor/baselines/display-validation.json",
|
||||
"test/browser/display-web.spec.js"
|
||||
],
|
||||
"resolutionRationale": "Session snapshots retain exact initial styles and parent/sibling positions through repeated entry, flag changes and reentry. Final installed UMD/legacy each pass 900 browser cases and exact-package consumers pass. Controlled platform paths do not certify physical devices; REL-03/REVIEW-02 retains that release gate."
|
||||
},
|
||||
{
|
||||
"id": "BASE-LIFE-17",
|
||||
"title": "Destroy in body web fullscreen leaves a connected orphan player",
|
||||
"closureCriteria": "Final installed matrices prove ownership restoration for both removeHtml values and reentrant destroy without late events.",
|
||||
"confirmation": "reproduced",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"CORE-16"
|
||||
],
|
||||
"evidence": [
|
||||
"test/browser/display-web.spec.js",
|
||||
"refactor/changes/2026-09-11-CORE-16-display.md"
|
||||
],
|
||||
"compatibleResolution": "Capture the first entry layout, restore original ownership on exit/destroy, preserve ordinary events, and stop obsolete operations after reentry.",
|
||||
"resolutionEvidence": [
|
||||
"refactor/changes/2026-09-11-CORE-16-display.md",
|
||||
"refactor/baselines/display-validation.json",
|
||||
"test/browser/display-web.spec.js"
|
||||
],
|
||||
"resolutionRationale": "Fullscreen body placement is restored before template destruction; a failed restore detaches only a still-displaced player while preserving the original error. Final installed UMD/legacy each pass 900 browser cases and exact-package consumers pass. Controlled platform paths do not certify physical devices; REL-03/REVIEW-02 retains that release gate."
|
||||
},
|
||||
{
|
||||
"id": "BASE-DOM-10",
|
||||
"title": "Native fullscreen state and exit affect unrelated player instances",
|
||||
"confirmation": "reproduced",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"CORE-16"
|
||||
],
|
||||
"evidence": [
|
||||
"test/browser/display-native.spec.js",
|
||||
"refactor/changes/2026-09-11-CORE-16-display.md"
|
||||
],
|
||||
"compatibleResolution": "Derive fullscreen from the instance player/video ownership, ignore unrelated changes, preserve normal event-before-class ordering, and exit only the owned element.",
|
||||
"closureCriteria": "Final installed UMD/legacy matrices cover real gesture entry, two live instances, fullscreen exit rejection/retry, subtitle integration and display mode composition.",
|
||||
"resolutionEvidence": [
|
||||
"refactor/changes/2026-09-11-CORE-16-display.md",
|
||||
"refactor/baselines/display-validation.json",
|
||||
"test/browser/display-native.spec.js"
|
||||
],
|
||||
"resolutionRationale": "Fullscreen ownership follows the actual instance player/video; real native tests cover two instances and mode transitions. Final installed UMD/legacy each pass 900 browser cases and exact-package consumers pass. Controlled platform paths do not certify physical devices; REL-03/REVIEW-02 retains that release gate."
|
||||
},
|
||||
{
|
||||
"id": "BASE-LIFE-18",
|
||||
"title": "Native fullscreen retains destroyed listeners and leaks rejected request listeners",
|
||||
"confirmation": "reproduced",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"CORE-16"
|
||||
],
|
||||
"evidence": [
|
||||
"test/browser/display-native.spec.js",
|
||||
"test/display-native.test.js",
|
||||
"refactor/changes/2026-09-11-CORE-16-display.md"
|
||||
],
|
||||
"compatibleResolution": "Scope per-instance and per-request listeners, preserve observable native rejection identity, settle cancelled requests promptly, and release late cancelled entry without retaining destroyed instances.",
|
||||
"closureCriteria": "Final installed matrices cover assignment errors, descriptor rejection, request cancellation/destroy, late Promise and controlled void APIs; document the weak document-lifetime fallback guard and physical-device limitations.",
|
||||
"resolutionEvidence": [
|
||||
"refactor/changes/2026-09-11-CORE-16-display.md",
|
||||
"refactor/baselines/display-validation.json",
|
||||
"test/browser/display-native.spec.js",
|
||||
"test/display-native.test.js"
|
||||
],
|
||||
"resolutionRationale": "Owned request listeners settle on success/failure/cancel, cancelled native entry is released, and void exit waits for both possible owned elements to leave. Final installed UMD/legacy each pass 900 browser cases and exact-package consumers pass. Controlled platform paths do not certify physical devices; REL-03/REVIEW-02 retains that release gate."
|
||||
},
|
||||
{
|
||||
"id": "BASE-DOM-11",
|
||||
"title": "Video-only fullscreen ignores native video state and begin/end transitions",
|
||||
"confirmation": "reproduced",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"CORE-16"
|
||||
],
|
||||
"evidence": [
|
||||
"test/browser/display-video-fullscreen.spec.js",
|
||||
"test/display-video-fullscreen.test.js"
|
||||
],
|
||||
"compatibleResolution": "Read native video presentation/display state and listen to video begin/end plus the historical document bridge, deduplicating the same transition while retaining the synchronous setter.",
|
||||
"closureCriteria": "Installed candidate regressions cover state, event order, reentry, PiP distinction and exit retry; record separately the physical iOS validation gate.",
|
||||
"resolutionEvidence": [
|
||||
"refactor/changes/2026-09-11-CORE-16-display.md",
|
||||
"refactor/baselines/display-validation.json",
|
||||
"test/browser/display-video-fullscreen.spec.js",
|
||||
"test/display-video-fullscreen.test.js"
|
||||
],
|
||||
"resolutionRationale": "Controlled native video presentation events and state distinguish fullscreen from PiP and deduplicate transitions without changing synchronous setters. Final installed UMD/legacy each pass 900 browser cases and exact-package consumers pass. Controlled platform paths do not certify physical devices; REL-03/REVIEW-02 retains that release gate."
|
||||
},
|
||||
{
|
||||
"id": "BASE-LIFE-19",
|
||||
"title": "Video-only fullscreen survives player destruction and late entry",
|
||||
"confirmation": "reproduced",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"CORE-16"
|
||||
],
|
||||
"evidence": [
|
||||
"test/browser/display-video-fullscreen.spec.js",
|
||||
"test/display-video-fullscreen.test.js"
|
||||
],
|
||||
"compatibleResolution": "Dispose instance listeners, exit owned video presentation, and leave only a static video-held cancellation guard for unabortable late entry without retaining the instance.",
|
||||
"closureCriteria": "Installed regressions cover active and pending destruction, exit failure behavior, new entry after cancellation and video shims without state properties; physical-device evidence remains explicit.",
|
||||
"resolutionEvidence": [
|
||||
"refactor/changes/2026-09-11-CORE-16-display.md",
|
||||
"refactor/baselines/display-validation.json",
|
||||
"test/browser/display-video-fullscreen.spec.js",
|
||||
"test/display-video-fullscreen.test.js"
|
||||
],
|
||||
"resolutionRationale": "Video-only instance listeners are removed at destruction; static video-held weak cancellation guards release late entry without holding the player scope. Final installed UMD/legacy each pass 900 browser cases and exact-package consumers pass. Controlled platform paths do not certify physical devices; REL-03/REVIEW-02 retains that release gate."
|
||||
},
|
||||
{
|
||||
"id": "BASE-DOM-12",
|
||||
"title": "PiP reports another video as active and WebKit ignores native presentation transitions",
|
||||
"confirmation": "reproduced",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"CORE-16"
|
||||
],
|
||||
"evidence": [
|
||||
"test/browser/display-pip.spec.js",
|
||||
"test/browser/display-pip-webkit.spec.js"
|
||||
],
|
||||
"compatibleResolution": "Keep native element/null and WebKit boolean return shapes, restrict native ownership, observe WebKit transitions and preserve repeated synchronous setter events.",
|
||||
"closureCriteria": "Installed matrices retain getter/setter contracts and prove native gestures/playback, transition deduplication, fullscreen composition and explicit unsupported capabilities.",
|
||||
"resolutionEvidence": [
|
||||
"refactor/changes/2026-09-11-CORE-16-display.md",
|
||||
"refactor/baselines/display-validation.json",
|
||||
"test/browser/display-pip.spec.js",
|
||||
"test/browser/display-pip-webkit.spec.js"
|
||||
],
|
||||
"resolutionRationale": "PiP retains native Element/null and WebKit boolean getters, ignores unrelated ownership and stale leave events, and observes native UI transitions. Final installed UMD/legacy each pass 900 browser cases and exact-package consumers pass. Controlled platform paths do not certify physical devices; REL-03/REVIEW-02 retains that release gate."
|
||||
},
|
||||
{
|
||||
"id": "BASE-LIFE-20",
|
||||
"title": "PiP survives destruction or late entry and rejected assignments become unhandled",
|
||||
"confirmation": "reproduced",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"CORE-16"
|
||||
],
|
||||
"evidence": [
|
||||
"test/browser/display-pip.spec.js",
|
||||
"test/browser/display-pip-webkit.spec.js",
|
||||
"test/display-pip.test.js"
|
||||
],
|
||||
"compatibleResolution": "Scope native listeners, restrict exit to owned presentation, suppress obsolete failure notices, and release late cancelled entry without instance-retaining WebKit guards.",
|
||||
"closureCriteria": "Installed matrices verify destroy/cancel, rejection notices, retry, reentry and cross-instance isolation; document that the historical void setter cannot expose a rejected Promise.",
|
||||
"resolutionEvidence": [
|
||||
"refactor/changes/2026-09-11-CORE-16-display.md",
|
||||
"refactor/baselines/display-validation.json",
|
||||
"test/browser/display-pip.spec.js",
|
||||
"test/browser/display-pip-webkit.spec.js",
|
||||
"test/display-pip.test.js"
|
||||
],
|
||||
"resolutionRationale": "PiP scope disposal exits owned presentation; late cancellation and rejected assignments are handled without stale notices or unhandled rejection. Final installed UMD/legacy each pass 900 browser cases and exact-package consumers pass. Controlled platform paths do not certify physical devices; REL-03/REVIEW-02 retains that release gate."
|
||||
},
|
||||
{
|
||||
"id": "BASE-TYPE-10",
|
||||
"title": "PiP getter is declared boolean but native runtime returns an element or null",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"owners": [
|
||||
"CORE-16",
|
||||
"CORE-21"
|
||||
],
|
||||
"evidence": [
|
||||
"test/browser/display-pip.spec.js",
|
||||
"test/types/display-pip.ts",
|
||||
"packages/artplayer/types/player.d.ts"
|
||||
],
|
||||
"compatibleResolution": "Keep old legal JS return identities and historical public consumers; model the internal branch-dependent getter accurately and coordinate a compatible public declaration strategy in CORE-21.",
|
||||
"closureCriteria": "Public declaration strategy and installed JS/TS consumers explicitly cover native element/null and fallback boolean without silently narrowing old legal consumption.",
|
||||
"workspaceState": "CORE-16 preserves historical native Element/null and WebKit boolean runtime returns with precise internal PipProperty. The installed old public consumers still pass; public declaration coordination remains CORE-21."
|
||||
},
|
||||
{
|
||||
"id": "BASE-DOM-13",
|
||||
"title": "Mini exit loses the video's original parent and sibling position",
|
||||
"confirmation": "reproduced",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"CORE-16"
|
||||
],
|
||||
"evidence": [
|
||||
"test/browser/display-mini.spec.js",
|
||||
"test/display-mini.test.js"
|
||||
],
|
||||
"compatibleResolution": "Capture original placement once per entry session, restore it on exit/destroy, preserve popup reuse and original DOM/display contracts, and retain the snapshot for failed-exit retry.",
|
||||
"closureCriteria": "Installed matrices cover placement and playback, reentry, recovery, caller-owned popups, narrow viewports and other display modes.",
|
||||
"resolutionEvidence": [
|
||||
"refactor/changes/2026-09-11-CORE-16-display.md",
|
||||
"refactor/baselines/display-validation.json",
|
||||
"test/browser/display-mini.spec.js",
|
||||
"test/display-mini.test.js"
|
||||
],
|
||||
"resolutionRationale": "Mini restores original parent/sibling snapshots and retains legacy first/reused DOM order, failure retry and caller-owned popup identity. Final installed UMD/legacy each pass 900 browser cases and exact-package consumers pass. Controlled platform paths do not certify physical devices; REL-03/REVIEW-02 retains that release gate."
|
||||
},
|
||||
{
|
||||
"id": "BASE-LIFE-21",
|
||||
"title": "Mini leaves a connected popup after player destruction",
|
||||
"confirmation": "reproduced",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"CORE-16"
|
||||
],
|
||||
"evidence": [
|
||||
"test/browser/display-mini.spec.js"
|
||||
],
|
||||
"compatibleResolution": "Own created popup and subscriptions in the instance scope, stop drag on hide/destroy, preserve caller-owned popup nodes, and serialize reentrant view creation.",
|
||||
"closureCriteria": "Installed matrices prove owned popup removal for both destroy modes, no late drag writes, active playback/controls, construction failure/abort recovery and at most one popup during reentry.",
|
||||
"resolutionEvidence": [
|
||||
"refactor/changes/2026-09-11-CORE-16-display.md",
|
||||
"refactor/baselines/display-validation.json",
|
||||
"test/browser/display-mini.spec.js"
|
||||
],
|
||||
"resolutionRationale": "Owned mini popup and subscriptions are removed at destruction; hiding cancels drag and reentrant creation yields at most one popup. Final installed UMD/legacy each pass 900 browser cases and exact-package consumers pass. Controlled platform paths do not certify physical devices; REL-03/REVIEW-02 retains that release gate."
|
||||
},
|
||||
{
|
||||
"id": "BASE-DOM-14",
|
||||
"title": "Unready media sizing partially overwrites layout and emits NaN height",
|
||||
"confirmation": "reproduced",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"CORE-16"
|
||||
],
|
||||
"evidence": [
|
||||
"test/browser/display-sizing.spec.js",
|
||||
"test/display-sizing.test.js",
|
||||
"refactor/baselines/display-sizing-validation.json"
|
||||
],
|
||||
"compatibleResolution": "Defer unusable geometry without invalid CSS or measurement events; recover on the next valid call while preserving normal sizing, descriptors, ratio parsing and event contracts.",
|
||||
"closureCriteria": "Installed matrices cover unready/hidden geometry, recovery, ratio/flip contracts, destruction and orientation composition.",
|
||||
"resolutionEvidence": [
|
||||
"refactor/changes/2026-09-11-CORE-16-display.md",
|
||||
"refactor/baselines/display-validation.json",
|
||||
"test/browser/display-sizing.spec.js",
|
||||
"test/display-sizing.test.js"
|
||||
],
|
||||
"resolutionRationale": "Unusable media/container dimensions defer layout and invalid measurements; normal geometry, later recovery, ratio/flip and destruction contracts are verified. Final installed UMD/legacy each pass 900 browser cases and exact-package consumers pass. Controlled platform paths do not certify physical devices; REL-03/REVIEW-02 retains that release gate."
|
||||
},
|
||||
{
|
||||
"id": "BASE-DOM-15",
|
||||
"title": "Auto orientation exit clears caller-owned inline dimensions and transforms",
|
||||
"confirmation": "reproduced",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"CORE-16"
|
||||
],
|
||||
"evidence": [
|
||||
"test/browser/display-orientation.spec.js",
|
||||
"refactor/baselines/display-orientation-validation.json"
|
||||
],
|
||||
"compatibleResolution": "Own rotation properties and coordinate restoration with fullscreenWeb; retain initial snapshots, repeated rotated dimensions and normal resize behavior.",
|
||||
"closureCriteria": "Installed matrices cover exit/destroy, repeated entry, rotated playback and ratio/flip composition; document device rotation and safe-area evidence limitations.",
|
||||
"resolutionEvidence": [
|
||||
"refactor/changes/2026-09-11-CORE-16-display.md",
|
||||
"refactor/baselines/display-validation.json",
|
||||
"test/browser/display-orientation.spec.js"
|
||||
],
|
||||
"resolutionRationale": "Rotation and fullscreen restoration preserve caller-owned inline geometry; repeated entry, both destruction modes and real rotated playback/ratio/flip are verified. Final installed UMD/legacy each pass 900 browser cases and exact-package consumers pass. Controlled platform paths do not certify physical devices; REL-03/REVIEW-02 retains that release gate."
|
||||
},
|
||||
{
|
||||
"id": "BASE-LIFE-22",
|
||||
"title": "Orientation lock success after fullscreen exit reactivates a cancelled player",
|
||||
"confirmation": "reproduced",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"CORE-16"
|
||||
],
|
||||
"evidence": [
|
||||
"test/browser/display-orientation.spec.js",
|
||||
"test/display-orientation.test.js",
|
||||
"refactor/baselines/display-orientation-validation.json"
|
||||
],
|
||||
"compatibleResolution": "Scope rotation timers and subscriptions, cancel locks on exit/destroy, release cancelled late success, and protect newer ArtPlayer requests with per-platform ownership tokens.",
|
||||
"closureCriteria": "Installed matrices and unit tests cover timer cancellation, late success/rejection, repeated entry, retry, destruction and cross-instance ownership; physical orientation remains an explicit device gate.",
|
||||
"resolutionEvidence": [
|
||||
"refactor/changes/2026-09-11-CORE-16-display.md",
|
||||
"refactor/baselines/display-validation.json",
|
||||
"test/browser/display-orientation.spec.js",
|
||||
"test/display-orientation.test.js"
|
||||
],
|
||||
"resolutionRationale": "Owned timers and subscriptions cancel cleanly; orientation request tokens prevent stale classes/notices and old-instance unlock of newer ArtPlayer locks. Final installed UMD/legacy each pass 900 browser cases and exact-package consumers pass. Controlled platform paths do not certify physical devices; REL-03/REVIEW-02 retains that release gate."
|
||||
}
|
||||
]
|
||||
}
|
||||
+5
-2
@@ -1049,11 +1049,14 @@
|
||||
"CORE-11",
|
||||
"CORE-12"
|
||||
],
|
||||
"status": "todo",
|
||||
"status": "done",
|
||||
"risk": "H",
|
||||
"deliverable": "显示模式服务和原公开属性门面",
|
||||
"acceptance": "模式互斥/恢复位置、手势权限、移动方向和退出失败通过",
|
||||
"evidence": []
|
||||
"evidence": [
|
||||
"changes/2026-09-11-CORE-16-display.md",
|
||||
"baselines/display-validation.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "CORE-17",
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
async function setup(page, core) {
|
||||
await page.goto(`/test/player.html?core=${core}&chapter=published`)
|
||||
await page.evaluate(() => {
|
||||
window.createPlayer('/test/pattern.mp4')
|
||||
window.miniEvents = []
|
||||
window.art.on('mini', value => window.miniEvents.push(value))
|
||||
const video = window.art.template.$video
|
||||
const parent = document.createElement('div')
|
||||
parent.id = 'original-video-parent'
|
||||
const next = document.createElement('span')
|
||||
next.id = 'original-video-next'
|
||||
video.before(parent)
|
||||
parent.append(video, next)
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
}
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`${core}: mini retains first-entry and reuse DOM order and display`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
art.mini = true
|
||||
const popup = art.template.$mini
|
||||
const first = popup.firstChild === art.template.$video
|
||||
const display = popup.style.display
|
||||
art.mini = true
|
||||
const result = { first, display, last: popup.lastChild === art.template.$video, reusedDisplay: popup.style.display }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ first: true, display: '', last: true, reusedDisplay: 'flex' })
|
||||
})
|
||||
|
||||
test(`${core}: mini restores the original video parent and sibling after repeated entry`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
art.mini = true
|
||||
const popup = art.template.$mini
|
||||
art.mini = true
|
||||
art.mini = false
|
||||
const result = { restored: art.template.$video.parentElement.id === 'original-video-parent', next: art.template.$video.nextSibling?.id === 'original-video-next', reused: popup === art.template.$mini, hidden: popup.style.display, events: [...window.miniEvents] }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ restored: core === 'candidate', next: core === 'candidate', reused: true, hidden: 'none', events: [true, true, false] })
|
||||
})
|
||||
|
||||
for (const removeHtml of [false, true]) {
|
||||
test(`${core}: destroying active mini removeHtml=${removeHtml} removes the owned popup`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate((removeHtml) => {
|
||||
const art = window.art
|
||||
art.mini = true
|
||||
const popup = art.template.$mini
|
||||
const video = art.template.$video
|
||||
window.miniEvents.length = 0
|
||||
art.destroy(removeHtml)
|
||||
return { popupConnected: popup.isConnected, videoInPopup: popup.contains(video), events: window.miniEvents }
|
||||
}, removeHtml)).toEqual({ popupConnected: core === 'published', videoInPopup: core === 'published', events: [] })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
test('candidate: hiding mini cancels drag and later document events cannot persist a hidden popup', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
const result = await page.evaluate(() => {
|
||||
const art = window.art
|
||||
art.mini = true
|
||||
const popup = art.template.$mini
|
||||
const original = { left: art.storage.get('left'), top: art.storage.get('top') }
|
||||
popup.dispatchEvent(new MouseEvent('mousedown', { button: 0, clientX: 20, clientY: 20 }))
|
||||
art.emit('document:mousemove', new MouseEvent('mousemove', { clientX: 40, clientY: 50 }))
|
||||
art.mini = false
|
||||
art.emit('document:mouseup', new MouseEvent('mouseup'))
|
||||
const result = { original, after: { left: art.storage.get('left'), top: art.storage.get('top') }, dragging: popup.classList.contains('art-mini-dragging'), transform: popup.style.transform }
|
||||
art.destroy()
|
||||
return result
|
||||
})
|
||||
expect(result.after).toEqual(result.original)
|
||||
expect(result.dragging).toBe(false)
|
||||
expect(result.transform).toBe('')
|
||||
})
|
||||
|
||||
test('candidate: mini remains in the viewport on small screens', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 280, height: 160 })
|
||||
await setup(page, 'candidate')
|
||||
await page.evaluate(() => {
|
||||
window.art.storage.set('left', 9000)
|
||||
window.art.storage.set('top', 9000)
|
||||
window.art.mini = true
|
||||
})
|
||||
const rect = await page.locator('.art-mini-popup').boundingBox()
|
||||
expect(rect.x).toBeGreaterThanOrEqual(0)
|
||||
expect(rect.y).toBeGreaterThanOrEqual(0)
|
||||
expect(rect.x + rect.width).toBeLessThanOrEqual(280)
|
||||
expect(rect.y + rect.height).toBeLessThanOrEqual(160)
|
||||
await page.evaluate(() => window.art.destroy())
|
||||
})
|
||||
|
||||
test('candidate: mini drag stores the actual position and playback buttons remain usable', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
await page.evaluate(() => window.art.mini = true)
|
||||
const popup = page.locator('.art-mini-popup')
|
||||
const rect = await popup.boundingBox()
|
||||
await page.mouse.move(rect.x + 30, rect.y + 20)
|
||||
await page.mouse.down()
|
||||
await page.mouse.move(rect.x - 30, rect.y - 20)
|
||||
await page.mouse.up()
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const rect = art.template.$mini.getBoundingClientRect()
|
||||
return Math.abs(art.storage.get('left') - rect.left) < 1 && Math.abs(art.storage.get('top') - rect.top) < 1
|
||||
})).toBe(true)
|
||||
await popup.locator('.art-icon-play').click()
|
||||
await expect.poll(() => page.evaluate(() => window.art.playing && window.art.currentTime > 0)).toBe(true)
|
||||
await popup.locator('.art-icon-pause').click()
|
||||
await expect.poll(() => page.evaluate(() => window.art.template.$video.paused)).toBe(true)
|
||||
await popup.locator('.art-mini-close').click()
|
||||
await expect(popup).toBeHidden()
|
||||
expect(await page.evaluate(() => window.art.template.$video.parentElement.id)).toBe('original-video-parent')
|
||||
await page.evaluate(() => window.art.destroy())
|
||||
})
|
||||
|
||||
test('candidate: mini exit failure keeps the active video and can restore on retry', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const parent = art.template.$video.parentNode
|
||||
art.mini = true
|
||||
const insert = parent.insertBefore
|
||||
const failure = new Error('controlled mini restore failure')
|
||||
parent.insertBefore = () => {
|
||||
throw failure
|
||||
}
|
||||
let rejected = false
|
||||
try {
|
||||
art.mini = false
|
||||
}
|
||||
catch (error) { rejected = error === failure }
|
||||
const active = art.mini && art.template.$mini.contains(art.template.$video)
|
||||
parent.insertBefore = insert
|
||||
art.mini = false
|
||||
const result = { rejected, active, restored: art.template.$video.parentNode === parent }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ rejected: true, active: true, restored: true })
|
||||
})
|
||||
|
||||
test('candidate: mini reentry during restoration retains the new placement snapshot', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const parent = art.template.$video.parentNode
|
||||
const insert = parent.insertBefore
|
||||
art.mini = true
|
||||
parent.insertBefore = function (...args) {
|
||||
const result = insert.apply(this, args)
|
||||
parent.insertBefore = insert
|
||||
art.mini = true
|
||||
return result
|
||||
}
|
||||
art.mini = false
|
||||
const active = art.mini
|
||||
art.mini = false
|
||||
const result = { active, restored: art.template.$video.parentNode === parent, events: [...window.miniEvents] }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ active: true, restored: true, events: [true, true, false] })
|
||||
})
|
||||
|
||||
for (const action of ['hide', 'destroy', 'fail']) {
|
||||
test(`candidate: mini construction ${action} releases incomplete UI and restores the video`, async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate((action) => {
|
||||
const art = window.art
|
||||
const icons = art.icons
|
||||
const failure = new Error('controlled mini icon failure')
|
||||
art.icons = {
|
||||
get close() {
|
||||
if (action === 'hide')
|
||||
art.mini = false
|
||||
else if (action === 'destroy')
|
||||
art.destroy(false)
|
||||
else
|
||||
throw failure
|
||||
return icons.close
|
||||
},
|
||||
get play() { return icons.play },
|
||||
get pause() { return icons.pause },
|
||||
}
|
||||
let rejected = false
|
||||
try {
|
||||
art.mini = true
|
||||
}
|
||||
catch (error) {
|
||||
rejected = error === failure
|
||||
}
|
||||
const restored = art.template.$video.parentElement.id === 'original-video-parent'
|
||||
const active = art.mini
|
||||
let retry = null
|
||||
if (action !== 'destroy') {
|
||||
art.icons = icons
|
||||
art.mini = true
|
||||
retry = art.mini && document.querySelectorAll('.art-mini-popup').length === 1
|
||||
art.destroy()
|
||||
}
|
||||
return { restored, active, rejected, retry, remaining: document.querySelectorAll('.art-mini-popup').length }
|
||||
}, action)).toEqual({ restored: true, active: false, rejected: action === 'fail', retry: action === 'destroy' ? null : true, remaining: 0 })
|
||||
})
|
||||
}
|
||||
|
||||
test('candidate: mini preserves a caller-owned popup on destruction', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const popup = document.createElement('div')
|
||||
popup.style.cssText = 'display: grid; width: 320px; height: 180px;'
|
||||
document.body.append(popup)
|
||||
art.template.$mini = popup
|
||||
art.mini = true
|
||||
art.destroy(false)
|
||||
const result = { connected: popup.isConnected, display: popup.style.display, restored: art.template.$video.parentElement.id === 'original-video-parent', empty: popup.childNodes.length === 0 }
|
||||
popup.remove()
|
||||
return result
|
||||
})).toEqual({ connected: true, display: 'grid', restored: true, empty: true })
|
||||
})
|
||||
|
||||
test('candidate: reentrant mini creation allocates only one owned popup', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const icons = art.icons
|
||||
let reentered = false
|
||||
art.icons = {
|
||||
get close() {
|
||||
if (!reentered) {
|
||||
reentered = true
|
||||
art.mini = true
|
||||
}
|
||||
return icons.close
|
||||
},
|
||||
get play() { return icons.play },
|
||||
get pause() { return icons.pause },
|
||||
}
|
||||
art.mini = true
|
||||
const result = { popups: document.querySelectorAll('.art-mini-popup').length, ownVideo: art.template.$mini.contains(art.template.$video), events: [...window.miniEvents] }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ popups: 1, ownVideo: true, events: [true] })
|
||||
})
|
||||
|
||||
test('candidate: mini switches to body web fullscreen and restores the original video placement', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
window.Artplayer.FULLSCREEN_WEB_IN_BODY = true
|
||||
art.mini = true
|
||||
art.fullscreenWeb = true
|
||||
const switched = !art.mini && art.fullscreenWeb && art.template.$mini.style.display === 'none'
|
||||
art.fullscreenWeb = false
|
||||
const restored = art.template.$video.parentElement.id === 'original-video-parent'
|
||||
art.mini = true
|
||||
art.mini = false
|
||||
const events = [...window.miniEvents]
|
||||
art.destroy()
|
||||
return { switched, restored, events }
|
||||
})).toEqual({ switched: true, restored: true, events: [true, false, true, false] })
|
||||
})
|
||||
|
||||
test('candidate: destroying a mini drag removes its document subscriptions', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
art.mini = true
|
||||
art.template.$mini.dispatchEvent(new MouseEvent('mousedown', { button: 0, clientX: 10, clientY: 10 }))
|
||||
art.destroy(false)
|
||||
let writes = 0
|
||||
art.storage.set = () => writes++
|
||||
art.emit('document:mousemove', new MouseEvent('mousemove', { clientX: 100, clientY: 100 }))
|
||||
art.emit('document:mouseup', new MouseEvent('mouseup'))
|
||||
return writes
|
||||
})).toBe(0)
|
||||
})
|
||||
@@ -0,0 +1,251 @@
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
async function setup(page, core) {
|
||||
await page.goto(`/test/player.html?core=${core}&chapter=published`)
|
||||
await page.evaluate(() => {
|
||||
const maps = [
|
||||
['requestFullscreen', 'exitFullscreen', 'fullscreenElement', 'fullscreenchange'],
|
||||
['webkitRequestFullscreen', 'webkitExitFullscreen', 'webkitFullscreenElement', 'webkitfullscreenchange'],
|
||||
['webkitRequestFullScreen', 'webkitCancelFullScreen', 'webkitCurrentFullScreenElement', 'webkitfullscreenchange'],
|
||||
['mozRequestFullScreen', 'mozCancelFullScreen', 'mozFullScreenElement', 'mozfullscreenchange'],
|
||||
['msRequestFullscreen', 'msExitFullscreen', 'msFullscreenElement', 'MSFullscreenChange'],
|
||||
]
|
||||
window.fullscreenAPI = maps.find(([, exit]) => exit in document)
|
||||
window.createPlayer('/test/pattern.mp4')
|
||||
window.art.template.$container.id = 'native-fullscreen-main'
|
||||
const container = document.createElement('div')
|
||||
Object.assign(container.style, { width: '320px', height: '180px' })
|
||||
document.body.append(container)
|
||||
window.other = new window.Artplayer({ container, url: '/test/pattern.mp4', muted: true })
|
||||
window.fullscreenEvents = [[], []]
|
||||
window.art.on('fullscreen', value => window.fullscreenEvents[0].push(value))
|
||||
window.other.on('fullscreen', value => window.fullscreenEvents[1].push(value))
|
||||
document.querySelector('#play').onclick = () => {
|
||||
window.art.fullscreen = true
|
||||
}
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady && window.other.isReady)).toBe(true)
|
||||
expect(await page.evaluate(() => Boolean(window.fullscreenAPI))).toBe(true)
|
||||
}
|
||||
|
||||
test('candidate: void exit waits for the owned video to leave and accepts another owner as completion', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate(async () => {
|
||||
const art = window.art
|
||||
const [, exitName, elementName, eventName] = window.fullscreenAPI
|
||||
const elementDescriptor = Object.getOwnPropertyDescriptor(document, elementName)
|
||||
const originalExit = document[exitName]
|
||||
let element = art.template.$video
|
||||
Object.defineProperty(document, elementName, { configurable: true, get: () => element })
|
||||
document.dispatchEvent(new Event(eventName))
|
||||
document[exitName] = () => undefined
|
||||
let settled = false
|
||||
const result = Object.getOwnPropertyDescriptor(art, 'fullscreen').set(false).then(() => settled = true)
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
const early = settled
|
||||
element = window.other.template.$player
|
||||
document.dispatchEvent(new Event(eventName))
|
||||
await result
|
||||
const completed = settled && !art.fullscreen && window.other.fullscreen
|
||||
element = null
|
||||
document.dispatchEvent(new Event(eventName))
|
||||
if (elementDescriptor)
|
||||
Object.defineProperty(document, elementName, elementDescriptor)
|
||||
else
|
||||
delete document[elementName]
|
||||
document[exitName] = originalExit
|
||||
art.destroy()
|
||||
window.other.destroy()
|
||||
return { early, completed }
|
||||
})).toEqual({ early: false, completed: true })
|
||||
})
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`${core}: native fullscreen belongs to its player and other players cannot exit it`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
await page.click('#play')
|
||||
await expect.poll(() => page.evaluate(() => document[window.fullscreenAPI[2]] === window.art.template.$player)).toBe(true)
|
||||
await expect.poll(() => page.evaluate(() => window.fullscreenEvents[0])).toEqual([true])
|
||||
expect(await page.evaluate(() => ({ own: window.art.fullscreen, other: window.other.fullscreen, className: window.other.template.$player.classList.contains('art-fullscreen'), otherEvents: window.fullscreenEvents[1] }))).toEqual({ own: true, other: core === 'published', className: core === 'published', otherEvents: core === 'published' ? [true] : [] })
|
||||
await page.evaluate(async () => {
|
||||
await Object.getOwnPropertyDescriptor(window.other, 'fullscreen').set.call(window.other, false)
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => Boolean(document[window.fullscreenAPI[2]]))).toBe(core === 'candidate')
|
||||
await page.evaluate(async () => {
|
||||
if (document[window.fullscreenAPI[2]])
|
||||
await document[window.fullscreenAPI[1]]()
|
||||
window.art.destroy()
|
||||
window.other.destroy()
|
||||
})
|
||||
})
|
||||
|
||||
test(`${core}: destroyed players stop receiving later native fullscreen events`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
await page.evaluate(() => window.other.destroy(false))
|
||||
await page.click('#play')
|
||||
await expect.poll(() => page.evaluate(() => window.fullscreenEvents[0])).toEqual([true])
|
||||
expect(await page.evaluate(() => ({ events: window.fullscreenEvents[1], className: window.other.template.$player.classList.contains('art-fullscreen') }))).toEqual({ events: core === 'published' ? [true] : [], className: core === 'published' })
|
||||
await page.evaluate(async () => {
|
||||
await document[window.fullscreenAPI[1]]()
|
||||
window.art.destroy()
|
||||
})
|
||||
})
|
||||
|
||||
test(`${core}: rejected native fullscreen request removes its temporary change listener`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(async () => {
|
||||
const art = window.art
|
||||
const [request, , , change] = window.fullscreenAPI
|
||||
const add = document.addEventListener
|
||||
const remove = document.removeEventListener
|
||||
const active = new Set()
|
||||
document.addEventListener = function (name, callback, options) {
|
||||
if (name === change)
|
||||
active.add(callback)
|
||||
return add.call(this, name, callback, options)
|
||||
}
|
||||
document.removeEventListener = function (name, callback, options) {
|
||||
if (name === change)
|
||||
active.delete(callback)
|
||||
return remove.call(this, name, callback, options)
|
||||
}
|
||||
const failure = new Error('controlled fullscreen rejection')
|
||||
art.template.$player[request] = () => Promise.reject(failure)
|
||||
const rejected = await Object.getOwnPropertyDescriptor(art, 'fullscreen').set.call(art, true).then(() => false, error => error === failure)
|
||||
const result = { rejected, leaked: active.size }
|
||||
document.addEventListener = add
|
||||
document.removeEventListener = remove
|
||||
art.destroy()
|
||||
window.other.destroy()
|
||||
return result
|
||||
})).toEqual({ rejected: true, leaked: core === 'published' ? 1 : 0 })
|
||||
})
|
||||
}
|
||||
|
||||
test('candidate: fullscreen assignment rejection shows notice without an unhandled rejection', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
await page.evaluate(() => {
|
||||
const [request] = window.fullscreenAPI
|
||||
window.art.template.$player[request] = () => Promise.reject(new Error('controlled assignment denial'))
|
||||
window.art.fullscreen = true
|
||||
})
|
||||
await expect(page.locator('#native-fullscreen-main .art-notice')).toContainText('controlled assignment denial')
|
||||
await page.evaluate(() => {
|
||||
window.art.destroy()
|
||||
window.other.destroy()
|
||||
})
|
||||
})
|
||||
|
||||
test('candidate: reentrant entry preserves real fullscreen UI and event order', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
await page.evaluate(() => {
|
||||
window.fullscreenOrder = []
|
||||
window.art.on('fullscreen', (value) => {
|
||||
if (value) {
|
||||
window.fullscreenOrder.push(['fullscreen', window.art.template.$player.classList.contains('art-fullscreen')])
|
||||
window.art.fullscreen = true
|
||||
}
|
||||
})
|
||||
window.art.on('resize', () => {
|
||||
if (window.art.fullscreen)
|
||||
window.fullscreenOrder.push(['resize', window.art.template.$player.classList.contains('art-fullscreen')])
|
||||
})
|
||||
})
|
||||
await page.click('#play')
|
||||
await expect.poll(() => page.evaluate(() => window.art.fullscreen && window.art.template.$player.classList.contains('art-fullscreen'))).toBe(true)
|
||||
expect(await page.evaluate(() => window.fullscreenOrder.slice(0, 2))).toEqual([['fullscreen', false], ['resize', true]])
|
||||
await page.evaluate(async () => {
|
||||
await document[window.fullscreenAPI[1]]()
|
||||
window.art.destroy()
|
||||
window.other.destroy()
|
||||
})
|
||||
})
|
||||
|
||||
for (const mode of ['promise-exit', 'promise-destroy', 'void-exit', 'void-destroy']) {
|
||||
test(`candidate: pending native entry ${mode} releases a late own entry`, async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate(async (mode) => {
|
||||
const art = window.art
|
||||
const [request, exit, element, change] = window.fullscreenAPI
|
||||
const originalExit = document[exit]
|
||||
const originalElement = Object.getOwnPropertyDescriptor(document, element)
|
||||
let current = null
|
||||
let complete
|
||||
let exits = 0
|
||||
Object.defineProperty(document, element, { configurable: true, get: () => current })
|
||||
art.template.$player[request] = () => {
|
||||
if (mode.startsWith('promise'))
|
||||
return new Promise(resolve => complete = resolve)
|
||||
complete = () => document.dispatchEvent(new Event(change))
|
||||
}
|
||||
document[exit] = () => {
|
||||
exits++
|
||||
current = null
|
||||
document.dispatchEvent(new Event(change))
|
||||
return Promise.resolve()
|
||||
}
|
||||
const setter = Object.getOwnPropertyDescriptor(art, 'fullscreen').set
|
||||
const pending = setter.call(art, true)
|
||||
if (mode.endsWith('destroy'))
|
||||
art.destroy(false)
|
||||
else
|
||||
await setter.call(art, false)
|
||||
await pending
|
||||
current = art.template.$player
|
||||
complete()
|
||||
// Flush the request's native Promise observer and setter settlement chain.
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
const result = { exits, fullscreen: art.fullscreen, className: art.template.$player.classList.contains('art-fullscreen'), events: window.fullscreenEvents[0] }
|
||||
art.destroy()
|
||||
window.other.destroy()
|
||||
document[exit] = originalExit
|
||||
if (originalElement)
|
||||
Object.defineProperty(document, element, originalElement)
|
||||
else
|
||||
delete document[element]
|
||||
return result
|
||||
}, mode)).toEqual({ exits: 1, fullscreen: false, className: false, events: [] })
|
||||
})
|
||||
}
|
||||
|
||||
test('candidate: cancelling an actual native request in the initiating click releases fullscreen', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
await page.evaluate(() => {
|
||||
window.entrySettled = false
|
||||
document.querySelector('#play').onclick = () => {
|
||||
const setter = Object.getOwnPropertyDescriptor(window.art, 'fullscreen').set
|
||||
const entering = setter.call(window.art, true)
|
||||
window.art.fullscreen = false
|
||||
void entering.then(() => window.entrySettled = true)
|
||||
}
|
||||
})
|
||||
await page.click('#play')
|
||||
await expect.poll(() => page.evaluate(() => window.entrySettled && !document[window.fullscreenAPI[2]])).toBe(true)
|
||||
await page.evaluate(() => {
|
||||
window.art.destroy()
|
||||
window.other.destroy()
|
||||
})
|
||||
})
|
||||
|
||||
test('candidate: rejected exit retains actual fullscreen state and can be retried', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
await page.click('#play')
|
||||
await expect.poll(() => page.evaluate(() => window.art.template.$player.classList.contains('art-fullscreen'))).toBe(true)
|
||||
expect(await page.evaluate(async () => {
|
||||
const [, exit] = window.fullscreenAPI
|
||||
const originalExit = document[exit]
|
||||
const failure = new Error('controlled exit denial')
|
||||
document[exit] = () => Promise.reject(failure)
|
||||
const setter = Object.getOwnPropertyDescriptor(window.art, 'fullscreen').set
|
||||
const rejected = await setter.call(window.art, false).then(() => false, error => error === failure)
|
||||
const result = { rejected, fullscreen: window.art.fullscreen, className: window.art.template.$player.classList.contains('art-fullscreen') }
|
||||
document[exit] = originalExit
|
||||
await setter.call(window.art, false)
|
||||
return result
|
||||
})).toEqual({ rejected: true, fullscreen: true, className: true })
|
||||
await expect.poll(() => page.evaluate(() => window.art.fullscreen || window.art.template.$player.classList.contains('art-fullscreen'))).toBe(false)
|
||||
await page.evaluate(() => {
|
||||
window.art.destroy()
|
||||
window.other.destroy()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,161 @@
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
async function setup(page, core) {
|
||||
await page.setViewportSize({ width: 400, height: 800 })
|
||||
await page.addInitScript(() => Object.defineProperty(navigator, 'userAgent', { configurable: true, value: 'ArtPlayer Android orientation fixture' }))
|
||||
await page.goto(`/test/player.html?core=${core}&chapter=published`)
|
||||
await page.evaluate(() => {
|
||||
window.Artplayer.AUTO_ORIENTATION_TIME = 10
|
||||
window.art = new window.Artplayer({ container: '.player', url: '/test/pattern.mp4', muted: true, autoOrientation: true })
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
}
|
||||
|
||||
test('candidate: a cancelled rotation timer cannot rotate a later fullscreen session early', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
await page.clock.install({ time: new Date('2026-09-11T00:00:00Z') })
|
||||
await page.clock.pauseAt(new Date('2026-09-11T00:00:01Z'))
|
||||
await page.evaluate(() => {
|
||||
window.Artplayer.AUTO_ORIENTATION_TIME = 1000
|
||||
window.art.fullscreenWeb = true
|
||||
})
|
||||
await page.clock.runFor(700)
|
||||
await page.evaluate(() => {
|
||||
window.art.fullscreenWeb = false
|
||||
window.art.fullscreenWeb = true
|
||||
})
|
||||
await page.clock.runFor(400)
|
||||
expect(await page.evaluate(() => window.art.plugins.autoOrientation.state)).toBe(false)
|
||||
await page.clock.runFor(600)
|
||||
expect(await page.evaluate(() => window.art.plugins.autoOrientation.state)).toBe(true)
|
||||
await page.evaluate(() => window.art.destroy())
|
||||
})
|
||||
|
||||
for (const removeHtml of [false, true]) {
|
||||
test(`candidate: rotation destruction restores styles and cancels subscriptions removeHtml=${removeHtml}`, async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
await page.evaluate(() => {
|
||||
const art = window.art
|
||||
art.constructor.FULLSCREEN_WEB_IN_BODY = true
|
||||
art.template.$player.style.cssText = 'width:75%;height:80%;transform:scale(0.9);transform-origin:center;'
|
||||
window.beforeRotation = art.template.$player.getAttribute('style')
|
||||
art.fullscreenWeb = true
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.plugins.autoOrientation.state)).toBe(true)
|
||||
expect(await page.evaluate((remove) => {
|
||||
const art = window.art
|
||||
const player = art.template.$player
|
||||
art.destroy(remove)
|
||||
const style = player.getAttribute('style')
|
||||
art.emit('fullscreenWeb', true)
|
||||
art.emit('fullscreen', true)
|
||||
return { restored: style === window.beforeRotation, rotated: art.isRotate, state: art.plugins.autoOrientation.state, retained: player.isConnected, changed: style !== player.getAttribute('style') }
|
||||
}, removeHtml)).toEqual({ restored: true, rotated: false, state: false, retained: !removeHtml, changed: false })
|
||||
})
|
||||
}
|
||||
|
||||
test('candidate: destroying a pending rotation removes its timer and produces no late resize', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
await page.clock.install({ time: new Date('2026-09-11T00:00:00Z') })
|
||||
await page.clock.pauseAt(new Date('2026-09-11T00:00:01Z'))
|
||||
await page.evaluate(() => {
|
||||
const art = window.art
|
||||
art.constructor.AUTO_ORIENTATION_TIME = 1000
|
||||
art.fullscreenWeb = true
|
||||
art.destroy(false)
|
||||
window.lateRotations = []
|
||||
art.on('resize', () => window.lateRotations.push(art.isRotate))
|
||||
})
|
||||
await page.clock.runFor(2000)
|
||||
expect(await page.evaluate(() => ({ rotated: window.art.plugins.autoOrientation.state, events: window.lateRotations }))).toEqual({ rotated: false, events: [] })
|
||||
})
|
||||
|
||||
test('candidate: rotated playback supports ratio and flip changes before restoring normal layout', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
await page.evaluate(async () => {
|
||||
await window.art.play()
|
||||
window.art.fullscreenWeb = true
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.plugins.autoOrientation.state && window.art.currentTime > 0.1)).toBe(true)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
art.aspectRatio = '4:3'
|
||||
art.flip = 'horizontal'
|
||||
const video = art.template.$video
|
||||
const ratio = video.clientWidth / video.clientHeight
|
||||
const flipped = art.flip
|
||||
art.fullscreenWeb = false
|
||||
art.aspectRatio = 'default'
|
||||
art.flip = 'normal'
|
||||
const result = { ratioClose: Math.abs(ratio - 4 / 3) < 0.01, flipped, playing: art.playing, rotate: art.isRotate, cleared: video.style.width === '' && video.style.height === '' }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ ratioClose: true, flipped: 'horizontal', playing: true, rotate: false, cleared: true })
|
||||
})
|
||||
|
||||
test('candidate: repeated web fullscreen retains rotated dimensions and final styles', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
await page.evaluate(() => {
|
||||
window.art.fullscreenWeb = true
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.plugins.autoOrientation.state)).toBe(true)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
art.fullscreenWeb = true
|
||||
const result = { width: art.template.$player.style.width, height: art.template.$player.style.height, state: art.plugins.autoOrientation.state }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ width: '800px', height: '400px', state: true })
|
||||
})
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`${core}: rotated web fullscreen restores original inline geometry`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
await page.evaluate(() => {
|
||||
const art = window.art
|
||||
art.template.$player.style.cssText = 'width:75%;height:80%;transform:scale(0.9);transform-origin:center;'
|
||||
window.beforeRotation = art.template.$player.getAttribute('style')
|
||||
art.fullscreenWeb = true
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.isRotate)).toBe(true)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const rotated = { state: art.plugins.autoOrientation.state, width: art.template.$player.style.width, height: art.template.$player.style.height }
|
||||
art.fullscreenWeb = false
|
||||
const original = document.createElement('div')
|
||||
original.setAttribute('style', window.beforeRotation)
|
||||
const restored = ['width', 'height', 'transform', 'transform-origin'].every(name => art.template.$player.style.getPropertyValue(name) === original.style.getPropertyValue(name))
|
||||
const result = { rotated, restored, state: art.plugins.autoOrientation.state, isRotate: art.isRotate }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ rotated: { state: true, width: '800px', height: '400px' }, restored: core === 'candidate', state: false, isRotate: false })
|
||||
})
|
||||
|
||||
test(`${core}: late orientation lock after fullscreen exit does not reactivate the player`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(async () => {
|
||||
const art = window.art
|
||||
const calls = []
|
||||
let resolveLock
|
||||
Object.defineProperty(screen, 'orientation', { configurable: true, value: {
|
||||
type: 'portrait-primary',
|
||||
lock(mode) {
|
||||
calls.push(mode)
|
||||
return new Promise((resolve) => {
|
||||
resolveLock = resolve
|
||||
})
|
||||
},
|
||||
unlock() {
|
||||
calls.push('unlock')
|
||||
},
|
||||
} })
|
||||
art.emit('fullscreen', true)
|
||||
art.emit('fullscreen', false)
|
||||
resolveLock()
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
const result = { requested: calls[0], unlocked: calls.includes('unlock'), active: art.template.$player.classList.contains('art-auto-orientation-fullscreen') }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ requested: 'landscape', unlocked: core === 'candidate', active: core !== 'candidate' })
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
async function setup(page, core) {
|
||||
await page.addInitScript(() => {
|
||||
Object.defineProperty(document, 'pictureInPictureEnabled', { configurable: true, value: false })
|
||||
const modes = new WeakMap()
|
||||
Object.defineProperties(HTMLVideoElement.prototype, {
|
||||
webkitSupportsPresentationMode: { configurable: true, writable: true, value: () => true },
|
||||
webkitPresentationMode: { configurable: true, get() { return modes.get(this) || 'inline' } },
|
||||
webkitSetPresentationMode: { configurable: true, writable: true, value(mode) {
|
||||
modes.set(this, mode)
|
||||
this.dispatchEvent(new Event('webkitpresentationmodechanged'))
|
||||
this.dispatchEvent(new Event(mode === 'picture-in-picture' ? 'enterpictureinpicture' : 'leavepictureinpicture'))
|
||||
} },
|
||||
})
|
||||
})
|
||||
await page.goto(`/test/player.html?core=${core}&chapter=published`)
|
||||
await page.evaluate(() => {
|
||||
window.createPlayer('/test/pattern.mp4')
|
||||
window.pipEvents = []
|
||||
window.art.on('pip', value => window.pipEvents.push(value))
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
}
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`${core}: WebKit PiP keeps boolean state and repeated synchronous setter events`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
art.pip = true
|
||||
art.pip = true
|
||||
const entered = art.pip
|
||||
art.pip = false
|
||||
const result = { entered, exited: art.pip, events: [...window.pipEvents] }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ entered: true, exited: false, events: [true, true, false] })
|
||||
})
|
||||
|
||||
test(`${core}: WebKit PiP follows native UI transitions`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const video = window.art.template.$video
|
||||
video.webkitSetPresentationMode('picture-in-picture')
|
||||
video.webkitSetPresentationMode('inline')
|
||||
const result = [...window.pipEvents]
|
||||
window.art.destroy()
|
||||
return result
|
||||
})).toEqual(core === 'candidate' ? [true, false] : [])
|
||||
})
|
||||
|
||||
test(`${core}: destroying WebKit PiP returns presentation to inline`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
window.art.pip = true
|
||||
window.pipEvents.length = 0
|
||||
window.art.destroy(false)
|
||||
return { mode: window.art.template.$video.webkitPresentationMode, events: window.pipEvents }
|
||||
})).toEqual({ mode: core === 'candidate' ? 'inline' : 'picture-in-picture', events: [] })
|
||||
})
|
||||
}
|
||||
|
||||
test('candidate: WebKit PiP pending entry is cleaned up after destruction', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const video = art.template.$video
|
||||
const setMode = video.webkitSetPresentationMode.bind(video)
|
||||
video.webkitSetPresentationMode = () => {}
|
||||
art.pip = true
|
||||
art.destroy(false)
|
||||
video.webkitSetPresentationMode = setMode
|
||||
setMode('picture-in-picture')
|
||||
return { mode: video.webkitPresentationMode, events: window.pipEvents }
|
||||
})).toEqual({ mode: 'inline', events: [] })
|
||||
})
|
||||
@@ -0,0 +1,161 @@
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
async function setup(page, core) {
|
||||
await page.addInitScript(() => {
|
||||
window.pipElement = null
|
||||
window.pipExits = 0
|
||||
Object.defineProperties(document, {
|
||||
pictureInPictureEnabled: { configurable: true, value: true },
|
||||
pictureInPictureElement: { configurable: true, get: () => window.pipElement },
|
||||
exitPictureInPicture: { configurable: true, writable: true, value() {
|
||||
window.pipExits++
|
||||
const previous = window.pipElement
|
||||
window.pipElement = null
|
||||
previous?.dispatchEvent(new Event('leavepictureinpicture'))
|
||||
return Promise.resolve()
|
||||
} },
|
||||
})
|
||||
Object.defineProperty(HTMLVideoElement.prototype, 'requestPictureInPicture', {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value() {
|
||||
window.pipElement = this
|
||||
this.dispatchEvent(new Event('enterpictureinpicture'))
|
||||
return Promise.resolve({ width: 320, height: 180 })
|
||||
},
|
||||
})
|
||||
})
|
||||
await page.goto(`/test/player.html?core=${core}&chapter=published`)
|
||||
await page.evaluate(() => {
|
||||
window.createPlayer('/test/pattern.mp4')
|
||||
window.pipEvents = []
|
||||
window.art.on('pip', value => window.pipEvents.push(value))
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
}
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`${core}: native PiP preserves element/null getter, void setter and boolean events`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const setter = Object.getOwnPropertyDescriptor(art, 'pip').set
|
||||
const entered = setter.call(art, true)
|
||||
const ownElement = art.pip === art.template.$video
|
||||
const exited = setter.call(art, false)
|
||||
const result = { ownElement, empty: art.pip === null, entered: typeof entered, exited: typeof exited, events: [...window.pipEvents] }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ ownElement: true, empty: true, entered: 'undefined', exited: 'undefined', events: [true, false] })
|
||||
})
|
||||
|
||||
test(`${core}: a player cannot report or exit another video's PiP`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const other = document.createElement('video')
|
||||
window.pipElement = other
|
||||
const reported = window.art.pip === other
|
||||
window.art.pip = false
|
||||
const result = { reported, stillOther: window.pipElement === other, exits: window.pipExits }
|
||||
window.pipElement = null
|
||||
window.art.destroy()
|
||||
return result
|
||||
})).toEqual({ reported: core === 'published', stillOther: core === 'candidate', exits: core === 'published' ? 1 : 0 })
|
||||
})
|
||||
|
||||
test(`${core}: destroy releases active PiP without a late player event`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(async () => {
|
||||
window.art.pip = true
|
||||
await Promise.resolve()
|
||||
window.pipEvents.length = 0
|
||||
window.art.destroy(false)
|
||||
await Promise.resolve()
|
||||
return { active: Boolean(window.pipElement), events: window.pipEvents }
|
||||
})).toEqual({ active: core === 'published', events: [] })
|
||||
})
|
||||
}
|
||||
|
||||
for (const action of ['cancel', 'destroy']) {
|
||||
test(`candidate: pending PiP ${action} releases late native entry`, async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate(async (action) => {
|
||||
const art = window.art
|
||||
const video = art.template.$video
|
||||
let complete
|
||||
video.requestPictureInPicture = () => new Promise(resolve => complete = resolve)
|
||||
art.pip = true
|
||||
if (action === 'destroy')
|
||||
art.destroy(false)
|
||||
else
|
||||
art.pip = false
|
||||
window.pipElement = video
|
||||
video.dispatchEvent(new Event('enterpictureinpicture'))
|
||||
complete({ width: 320, height: 180 })
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
const result = { active: Boolean(window.pipElement), events: [...window.pipEvents] }
|
||||
art.destroy()
|
||||
return result
|
||||
}, action)).toEqual({ active: false, events: [] })
|
||||
})
|
||||
}
|
||||
|
||||
test('candidate: rejected PiP assignment shows notice without an unhandled rejection', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
await page.evaluate(() => {
|
||||
window.art.template.$video.requestPictureInPicture = () => Promise.reject(new Error('controlled PiP denial'))
|
||||
window.art.pip = true
|
||||
})
|
||||
await expect(page.locator('.art-notice')).toContainText('controlled PiP denial')
|
||||
await page.evaluate(() => window.art.destroy())
|
||||
})
|
||||
|
||||
test('candidate: actual native PiP gesture and playback or explicit unsupported capability', async ({ page }, testInfo) => {
|
||||
await page.goto('/test/player.html?core=candidate&chapter=published')
|
||||
await page.evaluate(() => window.createPlayer('/test/pattern.mp4'))
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
const capability = await page.evaluate(() => ({
|
||||
enabled: document.pictureInPictureEnabled === true,
|
||||
request: typeof window.art.template.$video.requestPictureInPicture === 'function',
|
||||
exit: typeof document.exitPictureInPicture === 'function',
|
||||
}))
|
||||
await testInfo.attach('native-pip-capability', { contentType: 'application/json', body: JSON.stringify(capability) })
|
||||
if (!capability.enabled || !capability.request || !capability.exit) {
|
||||
expect(await page.evaluate(() => window.art.pip)).toBe(false)
|
||||
await page.evaluate(() => window.art.destroy())
|
||||
return
|
||||
}
|
||||
await page.evaluate(() => {
|
||||
const video = window.art.template.$video
|
||||
const request = video.requestPictureInPicture.bind(video)
|
||||
video.requestPictureInPicture = () => {
|
||||
const result = request()
|
||||
void result.then(pipWindow => window.actualPipWindow = { width: pipWindow.width, height: pipWindow.height })
|
||||
return result
|
||||
}
|
||||
document.querySelector('#play').onclick = () => {
|
||||
void window.art.play()
|
||||
window.art.pip = true
|
||||
}
|
||||
})
|
||||
await page.evaluate(() => window.art.mini = true)
|
||||
await page.click('#play')
|
||||
await expect.poll(() => page.evaluate(() => window.art.pip === window.art.template.$video)).toBe(true)
|
||||
expect(await page.evaluate(() => window.art.mini)).toBe(false)
|
||||
await expect.poll(() => page.evaluate(() => window.actualPipWindow?.width > 0 && window.art.currentTime > 0 && !window.art.template.$video.paused)).toBe(true)
|
||||
await testInfo.attach('native-pip-playback', { contentType: 'application/json', body: JSON.stringify(await page.evaluate(() => ({ window: window.actualPipWindow, currentTime: window.art.currentTime, events: window.events }))) })
|
||||
await page.evaluate(() => window.art.fullscreenWeb = true)
|
||||
await expect.poll(() => page.evaluate(() => document.pictureInPictureElement)).toBe(null)
|
||||
expect(await page.evaluate(() => window.art.fullscreenWeb)).toBe(true)
|
||||
await page.evaluate(() => window.art.fullscreenWeb = false)
|
||||
await page.click('#play')
|
||||
await expect.poll(() => page.evaluate(() => window.art.pip === window.art.template.$video)).toBe(true)
|
||||
await page.evaluate(() => {
|
||||
document.querySelector('#play').onclick = () => window.art.fullscreen = true
|
||||
})
|
||||
await page.click('#play')
|
||||
await expect.poll(() => page.evaluate(() => window.art.fullscreen && !document.pictureInPictureElement)).toBe(true)
|
||||
await page.evaluate(() => window.art.fullscreen = false)
|
||||
await expect.poll(() => page.evaluate(() => window.art.fullscreen)).toBe(false)
|
||||
await page.evaluate(() => window.art.destroy())
|
||||
})
|
||||
@@ -0,0 +1,161 @@
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
async function setup(page, core) {
|
||||
await page.goto(`/test/player.html?core=${core}&chapter=published`)
|
||||
await page.evaluate(() => window.createPlayer('/test/pattern.mp4'))
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
}
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`${core}: sizing methods remain detachable own properties and ratio parsing retains legacy coercion`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const descriptors = ['autoSize', 'autoHeight', 'aspectRatio', 'flip'].map((name) => {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(art, name)
|
||||
return { name, configurable: descriptor.configurable, enumerable: descriptor.enumerable, writable: descriptor.writable ?? null }
|
||||
})
|
||||
const { autoSize, autoHeight } = art
|
||||
const returns = [typeof Reflect.apply(autoSize, null, []), typeof Reflect.apply(autoHeight, {}, [])]
|
||||
art.template.$player.style.cssText = 'width:640px;height:360px;'
|
||||
const events = []
|
||||
art.on('aspectRatio', value => events.push(value))
|
||||
art.aspectRatio = ' 4 : 3 : ignored'
|
||||
const parsed = { value: art.aspectRatio, width: art.template.$video.style.width, height: art.template.$video.style.height }
|
||||
art.aspectRatio = ''
|
||||
art.aspectRatio = 'default'
|
||||
art.flip = 'custom'
|
||||
const custom = art.flip
|
||||
art.flip = ''
|
||||
const result = { descriptors, returns, parsed, events, custom, reset: art.flip }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({
|
||||
descriptors: ['autoSize', 'autoHeight', 'aspectRatio', 'flip'].map(name => ({ name, configurable: false, enumerable: false, writable: name.startsWith('auto') ? false : null })),
|
||||
returns: ['undefined', 'undefined'],
|
||||
parsed: { value: ' 4 : 3 : ignored', width: '480px', height: '100%' },
|
||||
events: [' 4 : 3 : ignored', 'default', 'default'],
|
||||
custom: 'custom',
|
||||
reset: 'normal',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
test('candidate: hidden containers defer sizing and recover after becoming visible', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const { $container, $video, $player } = art.template
|
||||
Object.defineProperties($video, { videoWidth: { configurable: true, value: 1920 }, videoHeight: { configurable: true, value: 1080 } })
|
||||
$container.style.cssText = 'display:none;width:640px;height:480px;'
|
||||
$player.style.cssText = 'width:80%;height:70%;'
|
||||
const events = []
|
||||
art.on('autoSize', value => events.push(['size', value]))
|
||||
art.on('autoHeight', value => events.push(['height', value]))
|
||||
art.autoSize()
|
||||
art.autoHeight()
|
||||
const hidden = { width: $player.style.width, height: $player.style.height, containerHeight: $container.style.height, count: events.length }
|
||||
$container.style.display = 'block'
|
||||
art.autoSize()
|
||||
art.autoHeight()
|
||||
const result = { hidden, events, height: $container.style.height }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ hidden: { width: '80%', height: '70%', containerHeight: '480px', count: 0 }, events: [['size', { width: 640, height: 360 }], ['height', 360]], height: '360px' })
|
||||
})
|
||||
|
||||
test('candidate: malformed ratios preserve layout while retaining notices, datasets and events', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const { $video } = art.template
|
||||
art.aspectRatio = '4:3'
|
||||
const before = $video.getAttribute('style')
|
||||
const values = []
|
||||
art.on('aspectRatio', value => values.push(value))
|
||||
const stable = ['bad', '0:0', '-1:2', '1:0', 'Infinity:2'].map((value) => {
|
||||
art.aspectRatio = value
|
||||
return $video.getAttribute('style') === before && art.aspectRatio === value && art.template.$notice.textContent.includes(value)
|
||||
})
|
||||
art.destroy()
|
||||
return { stable, values }
|
||||
})).toEqual({ stable: [true, true, true, true, true], values: ['bad', '0:0', '-1:2', '1:0', 'Infinity:2'] })
|
||||
})
|
||||
|
||||
test('candidate: retained display setters and sizing methods do not write or emit after destroy', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const { $container, $player, $video } = art.template
|
||||
art.destroy(false)
|
||||
const before = [$container.outerHTML, $player.outerHTML, $video.outerHTML]
|
||||
const events = []
|
||||
for (const name of ['autoSize', 'autoHeight', 'aspectRatio', 'flip'])
|
||||
art.on(name, () => events.push(name))
|
||||
art.autoSize()
|
||||
art.autoHeight()
|
||||
art.aspectRatio = '4:3'
|
||||
art.flip = 'horizontal'
|
||||
return { same: before.every((html, index) => html === [$container, $player, $video][index].outerHTML), events }
|
||||
})).toEqual({ same: true, events: [] })
|
||||
})
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`${core}: sizing preserves valid geometry, return values and emitted measurements`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const { $container, $player, $video } = art.template
|
||||
Object.defineProperties($video, { videoWidth: { configurable: true, value: 1920 }, videoHeight: { configurable: true, value: 1080 } })
|
||||
$container.style.cssText = 'width:640px;height:480px;'
|
||||
const sizes = []
|
||||
const heights = []
|
||||
art.on('autoSize', value => sizes.push(value))
|
||||
art.on('autoHeight', value => heights.push(value))
|
||||
const autoSize = art.autoSize()
|
||||
const styles = { width: $player.style.width, height: $player.style.height }
|
||||
const autoHeight = art.autoHeight()
|
||||
const result = { styles, sizes, heights, returns: [typeof autoSize, typeof autoHeight], containerHeight: $container.style.height }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ styles: { width: '100%', height: '75%' }, sizes: [{ width: 640, height: 360 }], heights: [360], returns: ['undefined', 'undefined'], containerHeight: '360px' })
|
||||
})
|
||||
|
||||
test(`${core}: aspect ratio and flip preserve dataset, notices and event order`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const events = []
|
||||
art.on('aspectRatio', value => events.push(['aspectRatio', value]))
|
||||
art.on('flip', value => events.push(['flip', value]))
|
||||
art.aspectRatio = '4:3'
|
||||
const video = art.template.$video
|
||||
const ratio = { width: video.style.width, height: video.style.height, margin: video.style.margin, value: art.aspectRatio }
|
||||
art.flip = 'horizontal'
|
||||
const flip = art.flip
|
||||
art.aspectRatio = 'default'
|
||||
art.flip = 'normal'
|
||||
const result = { ratio, flip, reset: { width: video.style.width, height: video.style.height, margin: video.style.margin, ratio: art.aspectRatio, flip: art.flip }, events }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ ratio: { width: '480px', height: '100%', margin: '0px auto', value: '4:3' }, flip: 'horizontal', reset: { width: '', height: '', margin: '', ratio: 'default', flip: 'normal' }, events: [['aspectRatio', '4:3'], ['flip', 'horizontal'], ['aspectRatio', 'default'], ['flip', 'normal']] })
|
||||
})
|
||||
|
||||
test(`${core}: unresolved media dimensions do not partially change layout or emit NaN height`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const { $video, $player } = art.template
|
||||
Object.defineProperties($video, { videoWidth: { configurable: true, value: 0 }, videoHeight: { configurable: true, value: 0 } })
|
||||
$player.style.width = '80%'
|
||||
$player.style.height = '70%'
|
||||
const events = []
|
||||
art.on('autoHeight', value => events.push(Number.isFinite(value)))
|
||||
art.autoSize()
|
||||
art.autoHeight()
|
||||
const result = { width: $player.style.width, height: $player.style.height, finiteHeightEvents: events }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ width: core === 'candidate' ? '80%' : '100%', height: '70%', finiteHeightEvents: core === 'candidate' ? [] : [false] })
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
async function setup(page, core) {
|
||||
await page.addInitScript(() => {
|
||||
// Exercise the video-only branch without claiming an iOS device test.
|
||||
for (const name of ['fullscreenEnabled', 'webkitFullscreenEnabled', 'webkitFullScreenEnabled', 'mozFullScreenEnabled', 'msFullscreenEnabled'])
|
||||
Object.defineProperty(document, name, { configurable: true, value: false })
|
||||
const state = new WeakMap()
|
||||
Object.defineProperties(HTMLVideoElement.prototype, {
|
||||
webkitSupportsFullscreen: { configurable: true, get: () => true },
|
||||
webkitDisplayingFullscreen: { configurable: true, get() { return state.get(this) === true } },
|
||||
webkitPresentationMode: { configurable: true, get() { return state.get(this) ? 'fullscreen' : 'inline' } },
|
||||
webkitEnterFullscreen: { configurable: true, writable: true, value() {
|
||||
state.set(this, true)
|
||||
this.dispatchEvent(new Event('webkitbeginfullscreen'))
|
||||
this.dispatchEvent(new Event('webkitpresentationmodechanged'))
|
||||
} },
|
||||
webkitExitFullscreen: { configurable: true, writable: true, value() {
|
||||
state.set(this, false)
|
||||
this.dispatchEvent(new Event('webkitendfullscreen'))
|
||||
this.dispatchEvent(new Event('webkitpresentationmodechanged'))
|
||||
} },
|
||||
})
|
||||
window.videoFullscreenState = state
|
||||
})
|
||||
await page.goto(`/test/player.html?core=${core}&chapter=published`)
|
||||
await page.evaluate(() => {
|
||||
window.createPlayer('/test/pattern.mp4')
|
||||
window.modeEvents = []
|
||||
window.art.on('fullscreen', value => window.modeEvents.push(value))
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
}
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`${core}: video-only fullscreen reads its native video state and deduplicates signals`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
art.fullscreen = true
|
||||
const entered = art.fullscreen
|
||||
art.emit('document:webkitfullscreenchange')
|
||||
art.fullscreen = false
|
||||
const result = { entered, exited: art.fullscreen, events: [...window.modeEvents] }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ entered: core === 'candidate', exited: false, events: core === 'candidate' ? [true, false] : [false] })
|
||||
})
|
||||
|
||||
test(`${core}: destroying video-only fullscreen releases native presentation`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
art.fullscreen = true
|
||||
window.modeEvents.length = 0
|
||||
art.destroy(false)
|
||||
return { native: art.template.$video.webkitDisplayingFullscreen, events: window.modeEvents }
|
||||
})).toEqual({ native: core === 'published', events: [] })
|
||||
})
|
||||
}
|
||||
|
||||
test('candidate: cancelled video-only entry after destroy is released without late player events', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const video = art.template.$video
|
||||
video.webkitEnterFullscreen = () => {}
|
||||
art.fullscreen = true
|
||||
art.destroy(false)
|
||||
window.videoFullscreenState.set(video, true)
|
||||
video.dispatchEvent(new Event('webkitbeginfullscreen'))
|
||||
return { native: video.webkitDisplayingFullscreen, events: window.modeEvents }
|
||||
})).toEqual({ native: false, events: [] })
|
||||
})
|
||||
|
||||
test('candidate: video-only fullscreen preserves synchronous errors and supports exit retry', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const video = art.template.$video
|
||||
art.fullscreen = true
|
||||
const exit = video.webkitExitFullscreen
|
||||
const failure = new Error('controlled video exit denial')
|
||||
video.webkitExitFullscreen = () => {
|
||||
throw failure
|
||||
}
|
||||
let rejected = false
|
||||
try {
|
||||
art.fullscreen = false
|
||||
}
|
||||
catch (error) { rejected = error === failure }
|
||||
const active = art.fullscreen
|
||||
video.webkitExitFullscreen = exit
|
||||
art.fullscreen = false
|
||||
const result = { rejected, active, exited: art.fullscreen, events: window.modeEvents }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ rejected: true, active: true, exited: false, events: [true, false] })
|
||||
})
|
||||
@@ -0,0 +1,233 @@
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
async function setup(page, core) {
|
||||
await page.goto(`/test/player.html?core=${core}&chapter=published`)
|
||||
await page.evaluate(() => window.createPlayer('/test/pattern.mp4'))
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
}
|
||||
|
||||
for (const removeHtml of [false, true]) {
|
||||
for (const movedBeforeThrow of [false, true]) {
|
||||
test(`candidate: failed destroy restoration cannot orphan the player removeHtml=${removeHtml} movedBeforeThrow=${movedBeforeThrow}`, async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate(({ removeHtml, movedBeforeThrow }) => {
|
||||
const art = window.art
|
||||
const { $player: player, $container: container } = art.template
|
||||
art.constructor.FULLSCREEN_WEB_IN_BODY = true
|
||||
art.fullscreenWeb = true
|
||||
const original = container.insertBefore
|
||||
const failure = new Error('destroy restore denied')
|
||||
container.insertBefore = function (node, anchor) {
|
||||
if (movedBeforeThrow)
|
||||
original.call(this, node, anchor)
|
||||
throw failure
|
||||
}
|
||||
let sameError = false
|
||||
try {
|
||||
art.destroy(removeHtml)
|
||||
}
|
||||
catch (error) {
|
||||
sameError = error === failure
|
||||
}
|
||||
container.insertBefore = original
|
||||
return { sameError, orphan: player.isConnected && !container.contains(player), retained: container.contains(player), destroyed: art.isDestroy, registered: art.constructor.instances.includes(art) }
|
||||
}, { removeHtml, movedBeforeThrow })).toEqual({ sameError: true, orphan: false, retained: movedBeforeThrow && !removeHtml, destroyed: true, registered: false })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
test('candidate: web fullscreen listener can exit without stale resize or overwritten restoration', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
const result = await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const player = art.template.$player
|
||||
player.style.width = '75%'
|
||||
const style = player.style.cssText
|
||||
const events = []
|
||||
art.on('fullscreenWeb', (value) => {
|
||||
events.push(value)
|
||||
if (value)
|
||||
art.fullscreenWeb = false
|
||||
})
|
||||
art.on('resize', () => events.push('resize'))
|
||||
art.fullscreenWeb = true
|
||||
const result = { events, active: art.fullscreenWeb, restored: player.style.cssText === style }
|
||||
art.destroy()
|
||||
return result
|
||||
})
|
||||
expect(result).toEqual({ events: [true, false, 'resize'], active: false, restored: true })
|
||||
})
|
||||
|
||||
test('candidate: web fullscreen listener destroy restores ownership without post-destroy resize', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const { $player: player, $container: container } = art.template
|
||||
art.constructor.FULLSCREEN_WEB_IN_BODY = true
|
||||
const style = player.style.cssText
|
||||
const events = []
|
||||
art.on('fullscreenWeb', () => art.destroy(false))
|
||||
art.on('destroy', () => events.push('destroy'))
|
||||
art.on('resize', () => events.push('resize'))
|
||||
art.fullscreenWeb = true
|
||||
art.fullscreenWeb = true
|
||||
return { events, retained: container.contains(player), style: player.style.cssText === style, fullscreen: player.classList.contains('art-fullscreen-web') }
|
||||
})).toEqual({ events: ['destroy'], retained: true, style: true, fullscreen: false })
|
||||
})
|
||||
|
||||
test('candidate: a failed body move restores style and keeps its original error', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const { $player: player, $container: container } = art.template
|
||||
const style = player.getAttribute('style')
|
||||
art.constructor.FULLSCREEN_WEB_IN_BODY = true
|
||||
const append = document.body.appendChild
|
||||
const failure = new Error('controlled body insertion failure')
|
||||
document.body.appendChild = function (node) {
|
||||
if (node === player)
|
||||
throw failure
|
||||
return append.call(this, node)
|
||||
}
|
||||
let sameError = false
|
||||
try {
|
||||
art.fullscreenWeb = true
|
||||
}
|
||||
catch (error) {
|
||||
sameError = error === failure
|
||||
}
|
||||
document.body.appendChild = append
|
||||
const result = { sameError, retained: container.contains(player), style: player.getAttribute('style') === style, active: art.fullscreenWeb }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ sameError: true, retained: true, style: true, active: false })
|
||||
})
|
||||
|
||||
test('candidate: a failed web fullscreen exit can retry restoring the original placement', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const { $player: player, $container: container } = art.template
|
||||
art.constructor.FULLSCREEN_WEB_IN_BODY = true
|
||||
const style = player.getAttribute('style')
|
||||
art.fullscreenWeb = true
|
||||
const insert = container.insertBefore
|
||||
const failure = new Error('controlled restore failure')
|
||||
container.insertBefore = () => {
|
||||
throw failure
|
||||
}
|
||||
let sameError = false
|
||||
try {
|
||||
art.fullscreenWeb = false
|
||||
}
|
||||
catch (error) {
|
||||
sameError = error === failure
|
||||
}
|
||||
container.insertBefore = insert
|
||||
art.fullscreenWeb = false
|
||||
const result = { sameError, retained: container.contains(player), style: player.getAttribute('style') === style, active: art.fullscreenWeb }
|
||||
art.destroy()
|
||||
return result
|
||||
})).toEqual({ sameError: true, retained: true, style: true, active: false })
|
||||
})
|
||||
|
||||
test('candidate: reentry during web fullscreen restoration preserves the newer active mode', async ({ page }) => {
|
||||
await setup(page, 'candidate')
|
||||
expect(await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const { $player: player, $container: container } = art.template
|
||||
art.constructor.FULLSCREEN_WEB_IN_BODY = true
|
||||
player.style.width = '65%'
|
||||
const style = player.getAttribute('style')
|
||||
art.fullscreenWeb = true
|
||||
const insert = container.insertBefore
|
||||
container.insertBefore = function (node, anchor) {
|
||||
container.insertBefore = insert
|
||||
const result = insert.call(this, node, anchor)
|
||||
art.fullscreenWeb = true
|
||||
return result
|
||||
}
|
||||
art.fullscreenWeb = false
|
||||
const winner = art.fullscreenWeb && player.parentNode === document.body && player.style.width === '100%'
|
||||
art.fullscreenWeb = false
|
||||
const restored = container.contains(player) && player.getAttribute('style') === style
|
||||
art.destroy()
|
||||
return { winner, restored }
|
||||
})).toEqual({ winner: true, restored: true })
|
||||
})
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
for (const initial of ['', 'width: 70%; height: 80%; color: red;']) {
|
||||
test(`${core}: web fullscreen restores initial style after repeated entry (${initial || 'empty'})`, async ({ page }, testInfo) => {
|
||||
await setup(page, core)
|
||||
const result = await page.evaluate((initial) => {
|
||||
const art = window.art
|
||||
art.constructor.FULLSCREEN_WEB_IN_BODY = false
|
||||
const player = art.template.$player
|
||||
player.style.cssText = initial
|
||||
const before = player.style.cssText
|
||||
const events = []
|
||||
let restoredAtExit
|
||||
art.on('fullscreenWeb', (value) => {
|
||||
events.push(value)
|
||||
if (!value)
|
||||
restoredAtExit = player.style.cssText
|
||||
})
|
||||
art.fullscreenWeb = true
|
||||
art.fullscreenWeb = true
|
||||
art.fullscreenWeb = false
|
||||
const result = { restored: restoredAtExit === before, events, active: art.fullscreenWeb, dimensions: [player.style.width, player.style.height, player.style.color], styles: { before, restoredAtExit, after: player.style.cssText } }
|
||||
art.destroy()
|
||||
return result
|
||||
}, initial)
|
||||
const { styles, ...observed } = result
|
||||
await testInfo.attach('fullscreen-styles', { contentType: 'application/json', body: JSON.stringify(styles) })
|
||||
expect(observed).toEqual({ restored: core === 'candidate', events: [true, true, false], active: false, dimensions: core === 'candidate' ? (initial ? ['70%', '80%', 'red'] : ['', '', '']) : ['100%', '100%', initial ? 'red' : ''] })
|
||||
})
|
||||
}
|
||||
|
||||
for (const changeFlag of [false, true]) {
|
||||
test(`${core}: body fullscreen restores original parent and sibling when flag changes=${changeFlag}`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
const result = await page.evaluate((changeFlag) => {
|
||||
const art = window.art
|
||||
const player = art.template.$player
|
||||
const wrapper = document.createElement('div')
|
||||
const sibling = document.createElement('span')
|
||||
art.template.$container.append(wrapper)
|
||||
wrapper.append(player, sibling)
|
||||
art.constructor.FULLSCREEN_WEB_IN_BODY = true
|
||||
art.fullscreenWeb = true
|
||||
const entered = player.parentNode === document.body
|
||||
if (changeFlag)
|
||||
art.constructor.FULLSCREEN_WEB_IN_BODY = false
|
||||
art.fullscreenWeb = false
|
||||
const result = { entered, parent: player.parentNode === wrapper, sibling: player.nextSibling === sibling, inBody: player.parentNode === document.body }
|
||||
art.destroy()
|
||||
player.remove()
|
||||
return result
|
||||
}, changeFlag)
|
||||
expect(result).toEqual({ entered: true, parent: core === 'candidate', sibling: core === 'candidate', inBody: core === 'published' && changeFlag })
|
||||
})
|
||||
}
|
||||
|
||||
for (const removeHtml of [false, true]) {
|
||||
test(`${core}: destroy while web fullscreen removeHtml=${removeHtml} restores DOM ownership`, async ({ page }) => {
|
||||
await setup(page, core)
|
||||
const result = await page.evaluate((removeHtml) => {
|
||||
const art = window.art
|
||||
const { $player: player, $container: container } = art.template
|
||||
const style = player.style.cssText
|
||||
art.constructor.FULLSCREEN_WEB_IN_BODY = true
|
||||
art.fullscreenWeb = true
|
||||
art.destroy(removeHtml)
|
||||
const result = { orphan: player.parentNode === document.body, retained: container.contains(player), style: player.style.cssText === style, fullscreen: player.classList.contains('art-fullscreen-web') }
|
||||
player.remove()
|
||||
return result
|
||||
}, removeHtml)
|
||||
expect(result).toEqual(core === 'candidate'
|
||||
? { orphan: false, retained: !removeHtml, style: true, fullscreen: false }
|
||||
: { orphan: true, retained: false, style: false, fullscreen: true })
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import assert from 'node:assert/strict'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Use the repository Node test runner.
|
||||
import { test } from 'node:test'
|
||||
import { loadModules } from './helpers/load.js'
|
||||
|
||||
const { miniPosition, clampMini } = await loadModules({
|
||||
miniPosition: { file: 'packages/artplayer/src/display/mini-layout', name: 'miniPosition' },
|
||||
clampMini: { file: 'packages/artplayer/src/display/mini-layout', name: 'clampMini' },
|
||||
})
|
||||
const geometry = { width: 320, height: 180, viewportWidth: 960, viewportHeight: 720 }
|
||||
|
||||
test('mini preserves finite visible stored coordinates including zero', () => {
|
||||
assert.deepEqual(miniPosition(0, 0, geometry), { left: 0, top: 0, reset: false })
|
||||
assert.deepEqual(miniPosition(640, 540, geometry), { left: 640, top: 540, reset: false })
|
||||
})
|
||||
|
||||
test('mini replaces missing, non-finite and offscreen storage with the legacy 50px inset', () => {
|
||||
for (const [left, top] of [[undefined, null], [Number.NaN, 20], [20, Infinity], ['20', 20], [-1, 50], [800, 400], [200, 700]])
|
||||
assert.deepEqual(miniPosition(left, top, geometry), { left: 590, top: 490, reset: true })
|
||||
})
|
||||
|
||||
test('mini default position remains reachable on small viewports', () => {
|
||||
assert.deepEqual(miniPosition(null, null, { width: 280, height: 160, viewportWidth: 280, viewportHeight: 160 }), { left: 0, top: 0, reset: true })
|
||||
})
|
||||
|
||||
test('mini drag clamps both boundaries without changing positions inside the viewport', () => {
|
||||
assert.deepEqual(clampMini(-100, 900, geometry), { left: 0, top: 540 })
|
||||
assert.deepEqual(clampMini(600, 400, geometry), { left: 600, top: 400 })
|
||||
assert.deepEqual(clampMini(900, -2, geometry), { left: 640, top: 0 })
|
||||
})
|
||||
@@ -0,0 +1,324 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { getEventListeners } from 'node:events'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Use the repository Node test runner.
|
||||
import { test } from 'node:test'
|
||||
import { setImmediate } from 'node:timers/promises'
|
||||
import { loadModules } from './helpers/load.js'
|
||||
|
||||
const { ResourceScope, requestFullscreen, nativeFullscreen, beginLifecycle, getScope } = await loadModules({
|
||||
ResourceScope: 'packages/artplayer/src/lifecycle/scope',
|
||||
requestFullscreen: { file: 'packages/artplayer/src/display/fullscreen-request', name: 'requestFullscreen' },
|
||||
nativeFullscreen: { file: 'packages/artplayer/src/display/native-fullscreen', name: 'nativeFullscreen' },
|
||||
beginLifecycle: { file: 'packages/artplayer/src/lifecycle/instance', name: 'beginLifecycle' },
|
||||
getScope: { file: 'packages/artplayer/src/lifecycle/instance', name: 'getScope' },
|
||||
})
|
||||
|
||||
function fixture() {
|
||||
const classes = new Set()
|
||||
const target = { classList: {
|
||||
remove: name => classes.delete(name),
|
||||
toggle: (name, value) => value ? classes.add(name) : classes.delete(name),
|
||||
} }
|
||||
const adapter = {
|
||||
document: new EventTarget(),
|
||||
target,
|
||||
element: null,
|
||||
elementProperty: 'fullscreenElement',
|
||||
changeEvent: 'fullscreenchange',
|
||||
errorEvent: 'fullscreenerror',
|
||||
request: () => Promise.resolve(),
|
||||
exit: () => Promise.resolve(),
|
||||
}
|
||||
Object.defineProperty(adapter.document, 'fullscreenElement', { get: () => adapter.element })
|
||||
const events = []
|
||||
const art = {
|
||||
template: { $player: target, $video: {} },
|
||||
notice: { show: '' },
|
||||
state: 'standard',
|
||||
emit: (...args) => events.push(args),
|
||||
}
|
||||
beginLifecycle(art)
|
||||
const descriptor = nativeFullscreen(art, adapter)
|
||||
const change = (element) => {
|
||||
adapter.element = element
|
||||
adapter.document.dispatchEvent(new Event(adapter.changeEvent))
|
||||
}
|
||||
const listeners = () => [adapter.changeEvent, adapter.errorEvent].map(name => getEventListeners(adapter.document, name).length)
|
||||
return { art, adapter, classes, descriptor, change, events, listeners }
|
||||
}
|
||||
|
||||
test('fullscreen native invocation retains the caller stack and original rejection', async () => {
|
||||
const { adapter } = fixture()
|
||||
const parent = new ResourceScope()
|
||||
const failure = new Error('denied')
|
||||
let called = false
|
||||
adapter.request = () => {
|
||||
called = true
|
||||
return Promise.reject(failure)
|
||||
}
|
||||
const operation = requestFullscreen(adapter, parent, true, () => assert.fail('not cancelled'))
|
||||
assert.equal(called, true)
|
||||
await assert.rejects(operation.promise, error => error === failure)
|
||||
assert.equal(getEventListeners(adapter.document, adapter.changeEvent).length, 1)
|
||||
assert.equal(getEventListeners(adapter.document, adapter.errorEvent).length, 1)
|
||||
parent.dispose()
|
||||
})
|
||||
|
||||
test('void fullscreen APIs wait for the actual change and remove operation listeners', async () => {
|
||||
const { adapter, change, listeners } = fixture()
|
||||
adapter.request = () => undefined
|
||||
const parent = new ResourceScope()
|
||||
const operation = requestFullscreen(adapter, parent, true, () => {})
|
||||
let settled = false
|
||||
void operation.promise.then(() => settled = true)
|
||||
await setImmediate()
|
||||
assert.equal(settled, false)
|
||||
assert.deepEqual(listeners(), [2, 2])
|
||||
change(adapter.target)
|
||||
await operation.promise
|
||||
assert.deepEqual(listeners(), [1, 1])
|
||||
parent.dispose()
|
||||
})
|
||||
|
||||
test('void native exit waits while the instance video still owns fullscreen', async () => {
|
||||
const { art, adapter, descriptor, change, listeners } = fixture()
|
||||
change(art.template.$video)
|
||||
adapter.exit = () => undefined
|
||||
let settled = false
|
||||
const exit = descriptor.set(false).then(() => settled = true)
|
||||
await setImmediate()
|
||||
assert.equal(settled, false)
|
||||
assert.deepEqual(listeners(), [2, 2])
|
||||
change(null)
|
||||
await exit
|
||||
assert.equal(settled, true)
|
||||
assert.deepEqual(listeners(), [1, 1])
|
||||
getScope(art).dispose()
|
||||
})
|
||||
|
||||
test('fullscreen request disposal settles without waiting and handles late completion once', async () => {
|
||||
const { adapter, listeners } = fixture()
|
||||
let complete
|
||||
let late = 0
|
||||
adapter.request = () => new Promise(resolve => complete = resolve)
|
||||
const parent = new ResourceScope()
|
||||
const operation = requestFullscreen(adapter, parent, true, () => late++)
|
||||
parent.dispose()
|
||||
await operation.promise
|
||||
assert.deepEqual(listeners(), [1, 1])
|
||||
complete()
|
||||
await setImmediate()
|
||||
assert.equal(late, 1)
|
||||
})
|
||||
|
||||
test('closed fullscreen request scopes never call the browser', async () => {
|
||||
const { adapter } = fixture()
|
||||
adapter.request = () => assert.fail('closed request invoked')
|
||||
const parent = new ResourceScope()
|
||||
parent.dispose()
|
||||
await requestFullscreen(adapter, parent, true, () => {}).promise
|
||||
})
|
||||
|
||||
test('native promise rejection retains its error when fullscreenerror arrives first', async () => {
|
||||
const { adapter } = fixture()
|
||||
const failure = new Error('browser permission denied')
|
||||
adapter.request = () => {
|
||||
adapter.document.dispatchEvent(new Event(adapter.errorEvent))
|
||||
return Promise.reject(failure)
|
||||
}
|
||||
const parent = new ResourceScope()
|
||||
await assert.rejects(requestFullscreen(adapter, parent, true, () => {}).promise, error => error === failure)
|
||||
parent.dispose()
|
||||
})
|
||||
|
||||
test('void fullscreen errors reject and release both temporary listeners', async () => {
|
||||
const { adapter, listeners } = fixture()
|
||||
adapter.request = () => undefined
|
||||
const parent = new ResourceScope()
|
||||
const operation = requestFullscreen(adapter, parent, true, () => {})
|
||||
adapter.document.dispatchEvent(new Event(adapter.errorEvent))
|
||||
await assert.rejects(operation.promise, /Fullscreen request failed/)
|
||||
assert.deepEqual(listeners(), [1, 1])
|
||||
parent.dispose()
|
||||
})
|
||||
|
||||
test('synchronous void fullscreen errors reject without waiting for a second event', async () => {
|
||||
const { adapter, listeners } = fixture()
|
||||
adapter.request = () => {
|
||||
adapter.document.dispatchEvent(new Event(adapter.errorEvent))
|
||||
}
|
||||
const parent = new ResourceScope()
|
||||
await assert.rejects(requestFullscreen(adapter, parent, true, () => {}).promise, /Fullscreen request failed/)
|
||||
assert.deepEqual(listeners(), [1, 1])
|
||||
parent.dispose()
|
||||
})
|
||||
|
||||
for (const close of ['exit', 'destroy']) {
|
||||
test(`pending fullscreen ${close} settles promptly and exits its late native entry`, async () => {
|
||||
const { art, adapter, descriptor, listeners } = fixture()
|
||||
let complete
|
||||
let exits = 0
|
||||
adapter.request = () => new Promise(resolve => complete = resolve)
|
||||
adapter.exit = () => {
|
||||
exits++
|
||||
adapter.element = null
|
||||
return Promise.resolve()
|
||||
}
|
||||
const entering = descriptor.set(true)
|
||||
if (close === 'destroy')
|
||||
getScope(art).dispose()
|
||||
else
|
||||
await descriptor.set(false)
|
||||
await entering
|
||||
assert.deepEqual(listeners(), close === 'destroy' ? [0, 0] : [1, 1])
|
||||
adapter.element = adapter.target
|
||||
complete()
|
||||
await setImmediate()
|
||||
assert.equal(exits, 1)
|
||||
assert.equal(descriptor.get(), false)
|
||||
})
|
||||
}
|
||||
|
||||
test('late cancelled fullscreen completion cannot exit another player', async () => {
|
||||
const { art, adapter, descriptor } = fixture()
|
||||
let complete
|
||||
adapter.request = () => new Promise(resolve => complete = resolve)
|
||||
adapter.exit = () => assert.fail('exited another player')
|
||||
const entering = descriptor.set(true)
|
||||
getScope(art).dispose()
|
||||
await entering
|
||||
adapter.element = {}
|
||||
complete()
|
||||
await setImmediate()
|
||||
})
|
||||
|
||||
test('cancelled void fullscreen requests retain no instance listeners and release late entry', async () => {
|
||||
const { art, adapter, descriptor, change, listeners, events } = fixture()
|
||||
let exits = 0
|
||||
adapter.request = () => undefined
|
||||
adapter.exit = () => {
|
||||
exits++
|
||||
adapter.element = null
|
||||
}
|
||||
const entering = descriptor.set(true)
|
||||
getScope(art).dispose()
|
||||
await entering
|
||||
// One shared document guard remains; both per-instance listeners are gone.
|
||||
assert.deepEqual(listeners(), [1, 0])
|
||||
change({})
|
||||
assert.equal(exits, 0)
|
||||
change(adapter.target)
|
||||
assert.equal(exits, 1)
|
||||
assert.equal(adapter.element, null)
|
||||
assert.deepEqual(events, [])
|
||||
})
|
||||
|
||||
test('new native entry supersedes an abandoned void request for the same target', async () => {
|
||||
const { art, adapter, descriptor, change } = fixture()
|
||||
adapter.request = () => undefined
|
||||
adapter.exit = () => assert.fail('new entry was mistaken for cancellation')
|
||||
const first = descriptor.set(true)
|
||||
await descriptor.set(false)
|
||||
await first
|
||||
const second = descriptor.set(true)
|
||||
change(adapter.target)
|
||||
await second
|
||||
assert.equal(descriptor.get(), true)
|
||||
adapter.element = null
|
||||
getScope(art).dispose()
|
||||
})
|
||||
|
||||
test('cancelled void entry never emits a stale true event or sets its fullscreen class', async () => {
|
||||
const { art, adapter, descriptor, change, events, classes } = fixture()
|
||||
adapter.request = () => undefined
|
||||
adapter.exit = () => change(null)
|
||||
const entering = descriptor.set(true)
|
||||
await descriptor.set(false)
|
||||
await entering
|
||||
change(adapter.target)
|
||||
assert.deepEqual(events, [])
|
||||
assert.equal(classes.has('art-fullscreen'), false)
|
||||
assert.equal(descriptor.get(), false)
|
||||
getScope(art).dispose()
|
||||
})
|
||||
|
||||
test('cancelled promise entry rejects a late change before its native promise resolves', async () => {
|
||||
const { art, adapter, descriptor, change, events, classes } = fixture()
|
||||
let complete
|
||||
adapter.request = () => new Promise(resolve => complete = resolve)
|
||||
adapter.exit = () => {
|
||||
change(null)
|
||||
return Promise.resolve()
|
||||
}
|
||||
const entering = descriptor.set(true)
|
||||
await descriptor.set(false)
|
||||
await entering
|
||||
change(adapter.target)
|
||||
complete()
|
||||
await setImmediate()
|
||||
assert.deepEqual(events, [])
|
||||
assert.equal(classes.has('art-fullscreen'), false)
|
||||
assert.equal(descriptor.get(), false)
|
||||
getScope(art).dispose()
|
||||
})
|
||||
|
||||
test('fullscreen rejection stays observable through the descriptor and reports notice', async () => {
|
||||
const { art, adapter, descriptor, listeners } = fixture()
|
||||
const failure = new Error('denied')
|
||||
adapter.request = () => Promise.reject(failure)
|
||||
await assert.rejects(descriptor.set(true), error => error === failure)
|
||||
assert.equal(art.notice.show, failure)
|
||||
assert.deepEqual(listeners(), [1, 1])
|
||||
getScope(art).dispose()
|
||||
})
|
||||
|
||||
test('fullscreen event precedes the class update and resize', async () => {
|
||||
const { art, adapter, descriptor, classes, change } = fixture()
|
||||
const observations = []
|
||||
art.emit = (name, value) => observations.push([name, value, classes.has('art-fullscreen')])
|
||||
adapter.request = () => {
|
||||
change(adapter.target)
|
||||
return Promise.resolve()
|
||||
}
|
||||
await descriptor.set(true)
|
||||
assert.deepEqual(observations, [['fullscreen', true, false], ['resize', undefined, true]])
|
||||
getScope(art).dispose()
|
||||
})
|
||||
|
||||
test('reentrant true during fullscreen entry still updates its class and resize', async () => {
|
||||
const { art, adapter, descriptor, classes, change, events } = fixture()
|
||||
art.emit = (...args) => {
|
||||
events.push(args)
|
||||
if (args[0] === 'fullscreen' && args[1])
|
||||
void descriptor.set(true)
|
||||
}
|
||||
adapter.request = () => {
|
||||
change(adapter.target)
|
||||
return Promise.resolve()
|
||||
}
|
||||
await descriptor.set(true)
|
||||
assert.equal(classes.has('art-fullscreen'), true)
|
||||
assert.deepEqual(events, [['fullscreen', true], ['resize']])
|
||||
getScope(art).dispose()
|
||||
})
|
||||
|
||||
test('reentrant exit during fullscreen entry prevents stale class and resize', async () => {
|
||||
const { art, adapter, descriptor, classes, change, events } = fixture()
|
||||
art.emit = (...args) => {
|
||||
events.push(args)
|
||||
if (args[0] === 'fullscreen' && args[1])
|
||||
void descriptor.set(false)
|
||||
}
|
||||
adapter.request = () => {
|
||||
change(adapter.target)
|
||||
return Promise.resolve()
|
||||
}
|
||||
adapter.exit = () => {
|
||||
change(null)
|
||||
return Promise.resolve()
|
||||
}
|
||||
await descriptor.set(true)
|
||||
assert.equal(classes.has('art-fullscreen'), false)
|
||||
assert.deepEqual(events, [['fullscreen', true], ['fullscreen', false], ['resize']])
|
||||
getScope(art).dispose()
|
||||
})
|
||||
@@ -0,0 +1,141 @@
|
||||
import assert from 'node:assert/strict'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Use the repository Node test runner.
|
||||
import { test } from 'node:test'
|
||||
import { setImmediate } from 'node:timers/promises'
|
||||
import { loadModules } from './helpers/load.js'
|
||||
|
||||
const { nativeOrientation, beginLifecycle, getScope } = await loadModules({
|
||||
nativeOrientation: { file: 'packages/artplayer/src/display/orientation-native', name: 'nativeOrientation' },
|
||||
beginLifecycle: { file: 'packages/artplayer/src/lifecycle/instance', name: 'beginLifecycle' },
|
||||
getScope: { file: 'packages/artplayer/src/lifecycle/instance', name: 'getScope' },
|
||||
})
|
||||
|
||||
function fixture(shared, need = () => true) {
|
||||
const calls = []
|
||||
const locks = []
|
||||
const orientation = shared || {
|
||||
type: 'portrait-primary',
|
||||
lock(mode) {
|
||||
calls.push(mode)
|
||||
return new Promise((resolve, reject) => locks.push({ resolve, reject }))
|
||||
},
|
||||
unlock() {
|
||||
calls.push('unlock')
|
||||
},
|
||||
}
|
||||
const classes = new Set()
|
||||
const art = {
|
||||
template: { $player: { classList: { add: name => classes.add(name), remove: name => classes.delete(name) }, ownerDocument: { defaultView: { screen: { orientation } } } } },
|
||||
notice: { show: '' },
|
||||
}
|
||||
beginLifecycle(art)
|
||||
return { art, orientation, calls, locks, classes, set: nativeOrientation(art, need), dispose: () => getScope(art).dispose() }
|
||||
}
|
||||
|
||||
test('orientation invokes synchronously, marks after success, and unlocks on exit', async () => {
|
||||
const f = fixture()
|
||||
assert.equal(f.set(true), undefined)
|
||||
assert.deepEqual(f.calls, ['landscape'])
|
||||
assert.equal(f.classes.size, 0)
|
||||
f.locks[0].resolve()
|
||||
await setImmediate()
|
||||
assert.deepEqual([...f.classes], ['art-auto-orientation-fullscreen'])
|
||||
f.set(false)
|
||||
assert.deepEqual(f.calls, ['landscape', 'unlock'])
|
||||
assert.equal(f.classes.size, 0)
|
||||
f.dispose()
|
||||
})
|
||||
|
||||
test('orientation repeated entry uses one pending or active lock', async () => {
|
||||
const f = fixture()
|
||||
f.set(true)
|
||||
f.set(true)
|
||||
f.locks[0].resolve()
|
||||
await setImmediate()
|
||||
f.set(true)
|
||||
assert.deepEqual(f.calls, ['landscape'])
|
||||
f.dispose()
|
||||
})
|
||||
|
||||
test('cancelled late orientation success is released without reactivating classes', async () => {
|
||||
const f = fixture()
|
||||
f.set(true)
|
||||
f.set(false)
|
||||
f.locks[0].resolve()
|
||||
await setImmediate()
|
||||
assert.deepEqual(f.calls, ['landscape', 'unlock', 'unlock'])
|
||||
assert.equal(f.classes.size, 0)
|
||||
assert.equal(f.art.notice.show, '')
|
||||
f.dispose()
|
||||
})
|
||||
|
||||
test('destroyed pending orientation ignores late rejection and further events', async () => {
|
||||
const f = fixture()
|
||||
f.set(true)
|
||||
f.dispose()
|
||||
f.art.notice.show = 'replacement notice'
|
||||
f.locks[0].reject(new Error('aborted'))
|
||||
await setImmediate()
|
||||
f.set(true)
|
||||
assert.deepEqual(f.calls, ['landscape', 'unlock'])
|
||||
assert.equal(f.art.notice.show, 'replacement notice')
|
||||
assert.equal(f.classes.size, 0)
|
||||
})
|
||||
|
||||
test('orientation failure preserves original error identity and allows retry', async () => {
|
||||
const f = fixture()
|
||||
const error = new Error('unsupported')
|
||||
f.set(true)
|
||||
f.locks[0].reject(error)
|
||||
await setImmediate()
|
||||
assert.equal(f.art.notice.show, error)
|
||||
f.orientation.type = 'landscape-primary'
|
||||
f.set(true)
|
||||
assert.deepEqual(f.calls, ['landscape', 'portrait'])
|
||||
f.locks[1].resolve()
|
||||
await setImmediate()
|
||||
f.dispose()
|
||||
})
|
||||
|
||||
test('orientation synchronous lock failures are reported and cleanup failure remains best effort', () => {
|
||||
const f = fixture()
|
||||
const error = new Error('sync denied')
|
||||
f.orientation.lock = () => {
|
||||
throw error
|
||||
}
|
||||
f.set(true)
|
||||
assert.equal(f.art.notice.show, error)
|
||||
f.orientation.lock = () => new Promise(() => {})
|
||||
f.orientation.unlock = () => {
|
||||
throw new Error('unlock denied')
|
||||
}
|
||||
f.set(true)
|
||||
assert.doesNotThrow(f.dispose)
|
||||
})
|
||||
|
||||
test('superseded orientation completion and old destruction cannot unlock a newer instance', async () => {
|
||||
const first = fixture()
|
||||
const second = fixture(first.orientation)
|
||||
first.set(true)
|
||||
second.set(true)
|
||||
first.locks[1].resolve()
|
||||
await setImmediate()
|
||||
first.locks[0].resolve()
|
||||
await setImmediate()
|
||||
first.dispose()
|
||||
assert.deepEqual(first.calls, ['landscape', 'landscape'])
|
||||
assert.equal(first.classes.size, 0)
|
||||
assert.equal(second.classes.size, 1)
|
||||
second.dispose()
|
||||
assert.deepEqual(first.calls, ['landscape', 'landscape', 'unlock'])
|
||||
})
|
||||
|
||||
test('no rotation requirement and incomplete platform capabilities do not call lock', () => {
|
||||
const f = fixture(undefined, () => false)
|
||||
f.set(true)
|
||||
assert.deepEqual(f.calls, [])
|
||||
f.dispose()
|
||||
const missing = fixture({ type: 'portrait-primary', lock: true })
|
||||
assert.doesNotThrow(() => missing.set(true))
|
||||
missing.dispose()
|
||||
})
|
||||
@@ -0,0 +1,215 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { getEventListeners } from 'node:events'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Use the repository Node test runner.
|
||||
import { test } from 'node:test'
|
||||
import { setImmediate } from 'node:timers/promises'
|
||||
import { loadModules } from './helpers/load.js'
|
||||
|
||||
const { Emitter, nativePip, webkitPip, beginLifecycle, getScope } = await loadModules({
|
||||
Emitter: 'packages/artplayer/src/utils/emitter',
|
||||
nativePip: { file: 'packages/artplayer/src/display/native-pip', name: 'nativePip' },
|
||||
webkitPip: { file: 'packages/artplayer/src/display/webkit-pip', name: 'webkitPip' },
|
||||
beginLifecycle: { file: 'packages/artplayer/src/lifecycle/instance', name: 'beginLifecycle' },
|
||||
getScope: { file: 'packages/artplayer/src/lifecycle/instance', name: 'getScope' },
|
||||
})
|
||||
|
||||
function fixture(kind = 'native') {
|
||||
const video = new EventTarget()
|
||||
video.ownerDocument = {
|
||||
pictureInPictureElement: null,
|
||||
exitPictureInPicture() {
|
||||
const previous = this.pictureInPictureElement
|
||||
this.pictureInPictureElement = null
|
||||
previous?.dispatchEvent(new Event('leavepictureinpicture'))
|
||||
return Promise.resolve()
|
||||
},
|
||||
}
|
||||
video.requestPictureInPicture = () => {
|
||||
video.ownerDocument.pictureInPictureElement = video
|
||||
video.dispatchEvent(new Event('enterpictureinpicture'))
|
||||
return Promise.resolve({ width: 320, height: 180 })
|
||||
}
|
||||
video.webkitPresentationMode = 'inline'
|
||||
video.webkitSetPresentationMode = (mode) => {
|
||||
video.webkitPresentationMode = mode
|
||||
video.dispatchEvent(new Event('webkitpresentationmodechanged'))
|
||||
video.dispatchEvent(new Event(mode === 'picture-in-picture' ? 'enterpictureinpicture' : 'leavepictureinpicture'))
|
||||
}
|
||||
const art = Object.assign(new Emitter(), { template: { $video: video }, state: 'standard', notice: { show: '' }, i18n: { get: key => key } })
|
||||
beginLifecycle(art)
|
||||
Object.defineProperty(art, 'pip', kind === 'native' ? nativePip(art) : webkitPip(art))
|
||||
const events = []
|
||||
art.on('pip', value => events.push(value))
|
||||
return { art, video, document: video.ownerDocument, events }
|
||||
}
|
||||
|
||||
test('native PiP invokes immediately, retains element identity and void setter', () => {
|
||||
const { art, video, events } = fixture()
|
||||
assert.equal(Object.getOwnPropertyDescriptor(art, 'pip').set(true), undefined)
|
||||
assert.equal(art.pip, video)
|
||||
assert.deepEqual(events, [true])
|
||||
art.pip = false
|
||||
assert.equal(art.pip, null)
|
||||
assert.deepEqual(events, [true, false])
|
||||
getScope(art).dispose()
|
||||
})
|
||||
|
||||
test('native PiP ignores a queued leave from an older session while the video still owns PiP', () => {
|
||||
const { art, video, events } = fixture()
|
||||
art.pip = true
|
||||
video.dispatchEvent(new Event('leavepictureinpicture'))
|
||||
assert.equal(art.pip, video)
|
||||
assert.deepEqual(events, [true])
|
||||
art.pip = false
|
||||
assert.deepEqual(events, [true, false])
|
||||
getScope(art).dispose()
|
||||
})
|
||||
|
||||
test('native PiP rejection preserves the notice error without unhandled rejection', async () => {
|
||||
const { art, video, events } = fixture()
|
||||
const failure = new Error('denied')
|
||||
video.requestPictureInPicture = () => Promise.reject(failure)
|
||||
art.pip = true
|
||||
await setImmediate()
|
||||
assert.equal(art.notice.show, failure)
|
||||
assert.equal(art.pip, null)
|
||||
assert.deepEqual(events, [])
|
||||
getScope(art).dispose()
|
||||
})
|
||||
|
||||
test('native PiP synchronous errors remain synchronous and retain identity', () => {
|
||||
const { art, video } = fixture()
|
||||
const failure = new Error('sync denial')
|
||||
video.requestPictureInPicture = () => {
|
||||
throw failure
|
||||
}
|
||||
assert.throws(() => {
|
||||
art.pip = true
|
||||
}, error => error === failure)
|
||||
getScope(art).dispose()
|
||||
})
|
||||
|
||||
test('obsolete native PiP failure does not replace the latest notice', async () => {
|
||||
const { art, video } = fixture()
|
||||
let reject
|
||||
video.requestPictureInPicture = () => new Promise((resolve, fail) => reject = fail)
|
||||
art.pip = true
|
||||
art.pip = false
|
||||
art.notice.show = 'current notice'
|
||||
reject(new Error('old request'))
|
||||
await setImmediate()
|
||||
assert.equal(art.notice.show, 'current notice')
|
||||
getScope(art).dispose()
|
||||
})
|
||||
|
||||
test('destroyed native PiP ignores late completion when another video owns PiP', async () => {
|
||||
const { art, video, document } = fixture()
|
||||
let complete
|
||||
video.requestPictureInPicture = () => new Promise(resolve => complete = resolve)
|
||||
art.pip = true
|
||||
getScope(art).dispose()
|
||||
document.pictureInPictureElement = {}
|
||||
document.exitPictureInPicture = () => assert.fail('exited another video')
|
||||
complete({})
|
||||
await setImmediate()
|
||||
assert.equal(getEventListeners(video, 'enterpictureinpicture').length, 0)
|
||||
assert.equal(getEventListeners(video, 'leavepictureinpicture').length, 0)
|
||||
})
|
||||
|
||||
test('native PiP exit rejection keeps real state and permits retry', async () => {
|
||||
const { art, document, video, events } = fixture()
|
||||
art.pip = true
|
||||
await setImmediate()
|
||||
const exit = document.exitPictureInPicture
|
||||
const failure = new Error('exit denied')
|
||||
document.exitPictureInPicture = () => Promise.reject(failure)
|
||||
art.pip = false
|
||||
await setImmediate()
|
||||
assert.equal(art.pip, video)
|
||||
assert.equal(art.notice.show, failure)
|
||||
document.exitPictureInPicture = exit
|
||||
art.pip = false
|
||||
await setImmediate()
|
||||
assert.equal(art.pip, null)
|
||||
assert.deepEqual(events, [true, false])
|
||||
getScope(art).dispose()
|
||||
})
|
||||
|
||||
test('WebKit PiP retains boolean getter and repeated synchronous setter events', () => {
|
||||
const { art, events } = fixture('webkit')
|
||||
art.pip = true
|
||||
art.pip = true
|
||||
assert.equal(art.pip, true)
|
||||
art.pip = false
|
||||
assert.equal(art.pip, false)
|
||||
assert.deepEqual(events, [true, true, false])
|
||||
getScope(art).dispose()
|
||||
})
|
||||
|
||||
test('WebKit PiP observes external presentation transitions without duplicate native signals', () => {
|
||||
const { art, video, events } = fixture('webkit')
|
||||
video.webkitSetPresentationMode('picture-in-picture')
|
||||
video.webkitSetPresentationMode('inline')
|
||||
assert.deepEqual(events, [true, false])
|
||||
getScope(art).dispose()
|
||||
})
|
||||
|
||||
test('WebKit PiP false does not exit a different fullscreen presentation', () => {
|
||||
const { art, video } = fixture('webkit')
|
||||
video.webkitPresentationMode = 'fullscreen'
|
||||
art.pip = false
|
||||
assert.equal(video.webkitPresentationMode, 'fullscreen')
|
||||
getScope(art).dispose()
|
||||
})
|
||||
|
||||
test('destroyed WebKit PiP releases delayed entry without retaining instance listeners', () => {
|
||||
const { art, video, events } = fixture('webkit')
|
||||
const setMode = video.webkitSetPresentationMode
|
||||
video.webkitSetPresentationMode = () => {}
|
||||
art.pip = true
|
||||
getScope(art).dispose()
|
||||
video.webkitSetPresentationMode = setMode
|
||||
setMode('picture-in-picture')
|
||||
assert.equal(video.webkitPresentationMode, 'inline')
|
||||
assert.deepEqual(events, [])
|
||||
assert.equal(getEventListeners(video, 'webkitpresentationmodechanged').length, 0)
|
||||
})
|
||||
|
||||
test('WebKit PiP destruction inside entry callback stops stale notifications', () => {
|
||||
const { art, video, events } = fixture('webkit')
|
||||
art.on('pip', () => getScope(art).dispose())
|
||||
art.pip = true
|
||||
assert.equal(video.webkitPresentationMode, 'inline')
|
||||
assert.deepEqual(events, [true])
|
||||
})
|
||||
|
||||
for (const kind of ['native', 'webkit']) {
|
||||
test(`${kind} PiP reentrant cancellation retains its branch-specific setter event contract`, async () => {
|
||||
const { art, video, events } = fixture(kind)
|
||||
// eslint-disable-next-line accessor-pairs -- Intercept mode synchronization without implementing the unrelated state getter.
|
||||
Object.defineProperty(art, 'state', { set(value) {
|
||||
if (value === 'pip')
|
||||
art.pip = false
|
||||
} })
|
||||
if (kind === 'native')
|
||||
await video.requestPictureInPicture()
|
||||
else
|
||||
video.webkitSetPresentationMode('picture-in-picture')
|
||||
assert.deepEqual(events, kind === 'native' ? [] : [false])
|
||||
getScope(art).dispose()
|
||||
})
|
||||
}
|
||||
|
||||
test('WebKit PiP checks current support and can retry after media becomes eligible', () => {
|
||||
const { art, video, events } = fixture('webkit')
|
||||
video.webkitSupportsPresentationMode = () => false
|
||||
art.pip = true
|
||||
assert.equal(art.pip, false)
|
||||
assert.equal(art.notice.show, 'PIP Not Supported')
|
||||
assert.deepEqual(events, [])
|
||||
video.webkitSupportsPresentationMode = () => true
|
||||
art.pip = true
|
||||
assert.equal(art.pip, true)
|
||||
assert.deepEqual(events, [true])
|
||||
getScope(art).dispose()
|
||||
})
|
||||
@@ -0,0 +1,43 @@
|
||||
import assert from 'node:assert/strict'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Use the repository Node test runner.
|
||||
import { test } from 'node:test'
|
||||
import { loadModules } from './helpers/load.js'
|
||||
|
||||
const { containSize, proportionalHeight } = await loadModules({
|
||||
containSize: { file: 'packages/artplayer/src/display/sizing', name: 'containSize' },
|
||||
proportionalHeight: { file: 'packages/artplayer/src/display/sizing', name: 'proportionalHeight' },
|
||||
})
|
||||
|
||||
test('contained media preserves its ratio, fits both bounds and fills one axis', () => {
|
||||
for (const container of [{ width: 640, height: 480 }, { width: 160, height: 900 }, { width: 0.5, height: 0.25 }]) {
|
||||
for (const ratio of [16 / 9, 9 / 16, 1, 4 / 3, 0.001, 1000]) {
|
||||
const size = containSize(container, ratio)
|
||||
assert.ok(size.width <= container.width && size.height <= container.height)
|
||||
assert.ok(Math.abs(size.width / size.height - ratio) <= ratio * 1e-12)
|
||||
assert.ok(size.width === container.width || size.height === container.height)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('zero, negative and non-finite geometry cannot produce layout writes', () => {
|
||||
for (const invalid of [0, -1, Number.NaN, Infinity, -Infinity]) {
|
||||
assert.equal(containSize({ width: invalid, height: 480 }, 16 / 9), undefined)
|
||||
assert.equal(containSize({ width: 640, height: invalid }, 16 / 9), undefined)
|
||||
assert.equal(containSize({ width: 640, height: 480 }, invalid), undefined)
|
||||
assert.equal(proportionalHeight(invalid, { width: 1920, height: 1080 }), undefined)
|
||||
assert.equal(proportionalHeight(640, { width: invalid, height: 1080 }), undefined)
|
||||
assert.equal(proportionalHeight(640, { width: 1920, height: invalid }), undefined)
|
||||
}
|
||||
})
|
||||
|
||||
test('auto height uses media proportions and recovers when dimensions become ready', () => {
|
||||
assert.equal(proportionalHeight(640, { width: 0, height: 0 }), undefined)
|
||||
assert.equal(proportionalHeight(640, { width: 1920, height: 1080 }), 360)
|
||||
assert.equal(proportionalHeight(360, { width: 1080, height: 1920 }), 640)
|
||||
})
|
||||
|
||||
test('overflow and underflow never escape as infinite or zero layout measurements', () => {
|
||||
assert.equal(proportionalHeight(Number.MAX_VALUE, { width: Number.MIN_VALUE, height: 1 }), undefined)
|
||||
assert.equal(proportionalHeight(Number.MIN_VALUE, { width: Number.MAX_VALUE, height: 1 }), undefined)
|
||||
assert.equal(containSize({ width: Number.MIN_VALUE, height: Number.MIN_VALUE }, Number.MAX_VALUE), undefined)
|
||||
})
|
||||
@@ -0,0 +1,151 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { getEventListeners } from 'node:events'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Use the repository Node test runner.
|
||||
import { test } from 'node:test'
|
||||
import { loadModules } from './helpers/load.js'
|
||||
|
||||
const { Emitter, videoFullscreen, beginLifecycle, getScope } = await loadModules({
|
||||
Emitter: 'packages/artplayer/src/utils/emitter',
|
||||
videoFullscreen: { file: 'packages/artplayer/src/display/video-fullscreen', name: 'videoFullscreen' },
|
||||
beginLifecycle: { file: 'packages/artplayer/src/lifecycle/instance', name: 'beginLifecycle' },
|
||||
getScope: { file: 'packages/artplayer/src/lifecycle/instance', name: 'getScope' },
|
||||
})
|
||||
|
||||
function fixture() {
|
||||
const video = new EventTarget()
|
||||
video.ownerDocument = { fullscreenElement: null }
|
||||
video.webkitDisplayingFullscreen = false
|
||||
video.webkitEnterFullscreen = function () {
|
||||
this.webkitDisplayingFullscreen = true
|
||||
this.dispatchEvent(new Event('webkitbeginfullscreen'))
|
||||
}
|
||||
video.webkitExitFullscreen = function () {
|
||||
this.webkitDisplayingFullscreen = false
|
||||
this.dispatchEvent(new Event('webkitendfullscreen'))
|
||||
}
|
||||
const art = Object.assign(new Emitter(), { template: { $video: video }, state: 'standard' })
|
||||
beginLifecycle(art)
|
||||
Object.defineProperty(art, 'fullscreen', videoFullscreen(art))
|
||||
const events = []
|
||||
art.on('fullscreen', value => events.push(['fullscreen', value]))
|
||||
art.on('resize', () => events.push(['resize']))
|
||||
return { art, video, events }
|
||||
}
|
||||
|
||||
test('video-only native callbacks preserve boolean state and normal event/resize order', () => {
|
||||
const { art, video, events } = fixture()
|
||||
art.fullscreen = true
|
||||
art.emit('document:webkitfullscreenchange')
|
||||
video.dispatchEvent(new Event('webkitpresentationmodechanged'))
|
||||
assert.equal(art.fullscreen, true)
|
||||
art.fullscreen = false
|
||||
assert.equal(art.fullscreen, false)
|
||||
assert.deepEqual(events, [['fullscreen', true], ['resize'], ['fullscreen', false], ['resize']])
|
||||
getScope(art).dispose()
|
||||
assert.equal(getEventListeners(video, 'webkitbeginfullscreen').length, 0)
|
||||
assert.equal(getEventListeners(video, 'webkitendfullscreen').length, 0)
|
||||
assert.equal(getEventListeners(video, 'webkitpresentationmodechanged').length, 0)
|
||||
})
|
||||
|
||||
test('video presentation PiP is distinct from fullscreen even if displayingFullscreen is true', () => {
|
||||
const { art, video, events } = fixture()
|
||||
video.webkitPresentationMode = 'picture-in-picture'
|
||||
video.webkitDisplayingFullscreen = true
|
||||
video.dispatchEvent(new Event('webkitpresentationmodechanged'))
|
||||
assert.equal(art.fullscreen, false)
|
||||
assert.deepEqual(events, [])
|
||||
getScope(art).dispose()
|
||||
})
|
||||
|
||||
test('video fullscreen falls back to native events if a shim has no state property', () => {
|
||||
const { art, video } = fixture()
|
||||
delete video.webkitDisplayingFullscreen
|
||||
video.dispatchEvent(new Event('webkitbeginfullscreen'))
|
||||
assert.equal(art.fullscreen, true)
|
||||
video.dispatchEvent(new Event('webkitendfullscreen'))
|
||||
assert.equal(art.fullscreen, false)
|
||||
getScope(art).dispose()
|
||||
})
|
||||
|
||||
test('video fullscreen destruction during its entry event stops stale resize and exits', () => {
|
||||
const { art, video, events } = fixture()
|
||||
art.on('fullscreen', (value) => {
|
||||
if (value)
|
||||
getScope(art).dispose()
|
||||
})
|
||||
art.fullscreen = true
|
||||
assert.equal(video.webkitDisplayingFullscreen, false)
|
||||
assert.equal(art.fullscreen, false)
|
||||
assert.deepEqual(events, [['fullscreen', true]])
|
||||
})
|
||||
|
||||
test('video fullscreen same-state reentry preserves the one resize', () => {
|
||||
const { art, events } = fixture()
|
||||
art.on('fullscreen', (value) => {
|
||||
if (value)
|
||||
art.fullscreen = true
|
||||
})
|
||||
art.fullscreen = true
|
||||
assert.equal(art.fullscreen, true)
|
||||
assert.deepEqual(events, [['fullscreen', true], ['resize']])
|
||||
getScope(art).dispose()
|
||||
})
|
||||
|
||||
test('video fullscreen synchronous request errors keep their identity and allow retry', () => {
|
||||
const { art, video } = fixture()
|
||||
const enter = video.webkitEnterFullscreen
|
||||
const error = new Error('denied')
|
||||
video.webkitEnterFullscreen = () => {
|
||||
throw error
|
||||
}
|
||||
assert.throws(() => {
|
||||
art.fullscreen = true
|
||||
}, actual => actual === error)
|
||||
video.webkitEnterFullscreen = enter
|
||||
art.fullscreen = true
|
||||
assert.equal(art.fullscreen, true)
|
||||
getScope(art).dispose()
|
||||
})
|
||||
|
||||
test('cancelled video fullscreen entry is released without stale player events', () => {
|
||||
const { art, video, events } = fixture()
|
||||
const enter = video.webkitEnterFullscreen
|
||||
video.webkitEnterFullscreen = () => {}
|
||||
art.fullscreen = true
|
||||
art.fullscreen = false
|
||||
enter.call(video)
|
||||
assert.equal(video.webkitDisplayingFullscreen, false)
|
||||
assert.equal(art.fullscreen, false)
|
||||
assert.deepEqual(events, [])
|
||||
getScope(art).dispose()
|
||||
})
|
||||
|
||||
test('new video fullscreen entry supersedes the previous cancellation', () => {
|
||||
const { art, video, events } = fixture()
|
||||
const enter = video.webkitEnterFullscreen
|
||||
video.webkitEnterFullscreen = () => {}
|
||||
art.fullscreen = true
|
||||
art.fullscreen = false
|
||||
video.webkitEnterFullscreen = enter
|
||||
art.fullscreen = true
|
||||
assert.equal(art.fullscreen, true)
|
||||
assert.deepEqual(events, [['fullscreen', true], ['resize']])
|
||||
getScope(art).dispose()
|
||||
})
|
||||
|
||||
test('late entry on a video shim without state properties is released after destroy', () => {
|
||||
const { art, video, events } = fixture()
|
||||
delete video.webkitDisplayingFullscreen
|
||||
let exits = 0
|
||||
video.webkitEnterFullscreen = () => {}
|
||||
video.webkitExitFullscreen = () => {
|
||||
exits++
|
||||
video.dispatchEvent(new Event('webkitendfullscreen'))
|
||||
}
|
||||
art.fullscreen = true
|
||||
getScope(art).dispose()
|
||||
video.dispatchEvent(new Event('webkitbeginfullscreen'))
|
||||
assert.equal(exits, 1)
|
||||
assert.deepEqual(events, [])
|
||||
assert.equal(getEventListeners(video, 'webkitbeginfullscreen').length, 0)
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { MiniHost } from '../../packages/artplayer/src/display/types'
|
||||
import miniMix from '../../packages/artplayer/src/player/miniMix'
|
||||
|
||||
declare const host: MiniHost
|
||||
miniMix(host)
|
||||
const active: boolean = host.mini
|
||||
host.mini = true
|
||||
host.mini = false
|
||||
// @ts-expect-error Mini retains boolean setters.
|
||||
host.mini = 'true'
|
||||
// @ts-expect-error Mini storage persists numeric coordinates.
|
||||
host.storage.set('left', '25px')
|
||||
export { active }
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { OrientationHost, OrientationLock } from '../../packages/artplayer/src/display/orientation-types'
|
||||
import autoOrientation from '../../packages/artplayer/src/plugins/autoOrientation'
|
||||
|
||||
declare const host: OrientationHost
|
||||
declare const lock: OrientationLock
|
||||
const plugin = autoOrientation(host)
|
||||
const state: boolean = plugin.state
|
||||
// @ts-expect-error The builtin exposes a getter, not a writable flag.
|
||||
plugin.state = true
|
||||
// @ts-expect-error Internal orientation requests use the opposite portrait/landscape mode.
|
||||
lock.lock('upside-down')
|
||||
// @ts-expect-error Fullscreen events carry boolean state.
|
||||
host.on('fullscreen', (value: string) => value)
|
||||
export { state }
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { PipHost } from '../../packages/artplayer/src/display/types'
|
||||
import pipMix from '../../packages/artplayer/src/player/pipMix'
|
||||
|
||||
declare const host: PipHost
|
||||
pipMix(host)
|
||||
const state: HTMLElement | null | boolean = host.pip
|
||||
// @ts-expect-error Native PiP historically returns the element or null, not just boolean.
|
||||
const booleanOnly: boolean = host.pip
|
||||
host.pip = true
|
||||
host.pip = false
|
||||
// @ts-expect-error PiP setters accept a boolean input.
|
||||
host.pip = 'picture-in-picture'
|
||||
export { booleanOnly, state }
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { AutoHeightHost, AutoSizeHost } from '../../packages/artplayer/src/display/sizing-types'
|
||||
import { containSize, proportionalHeight } from '../../packages/artplayer/src/display/sizing'
|
||||
import autoHeightMix from '../../packages/artplayer/src/player/autoHeightMix'
|
||||
import autoSizeMix from '../../packages/artplayer/src/player/autoSizeMix'
|
||||
|
||||
declare const sizeHost: AutoSizeHost
|
||||
declare const heightHost: AutoHeightHost
|
||||
autoSizeMix(sizeHost)
|
||||
autoHeightMix(heightHost)
|
||||
const height: number | undefined = proportionalHeight(640, { width: 1920, height: 1080 })
|
||||
const size = containSize({ width: 640, height: 480 }, 16 / 9)
|
||||
if (size) {
|
||||
const axis: 'width' | 'height' = size.axis
|
||||
sizeHost.emit('autoSize', { width: size.width, height: size.height })
|
||||
void axis
|
||||
}
|
||||
// @ts-expect-error The internal measurement cannot be a CSS string.
|
||||
heightHost.emit('autoHeight', '360px')
|
||||
// @ts-expect-error Dimensions must describe both axes.
|
||||
proportionalHeight(640, { width: 1920 })
|
||||
export { height }
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { WebFullscreenHost } from '../../packages/artplayer/src/display/types'
|
||||
import { capturePlacement, restorePlacement } from '../../packages/artplayer/src/display/placement'
|
||||
import fullscreenWebMix from '../../packages/artplayer/src/player/fullscreenWebMix'
|
||||
|
||||
declare const host: WebFullscreenHost
|
||||
fullscreenWebMix(host)
|
||||
host.fullscreenWeb = true
|
||||
const active: boolean = host.fullscreenWeb
|
||||
const placement = capturePlacement(host.template.$player)
|
||||
restorePlacement(placement)
|
||||
// @ts-expect-error Display setters retain a boolean input.
|
||||
host.fullscreenWeb = 'true'
|
||||
// @ts-expect-error DOM placement cannot be constructed from a string.
|
||||
capturePlacement('player')
|
||||
export { active }
|
||||
Reference in new issue
Block a user