mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-19 19:51:20 -08:00
refactor(chromecast): [PKG-CAST-03] isolate SDK and player lifecycles
This commit is contained in:
1 parent
87cfcf5a6c
commit
b0cfbfe3a0
26 files changed
+2503
-551
No files matched your search
@@ -0,0 +1,131 @@
|
||||
# Chromecast maintenance
|
||||
|
||||
The factory remains `artplayerPluginChromecast(option)`. Registration is async and
|
||||
adds the `chromecast` right control immediately. Its resolved object retains
|
||||
`name`, `getCastState()` and `isCasting()`. SDK loading remains lazy on first click.
|
||||
The option object stays live: later changes to `url`, `mimeType`, `sdk` and callback
|
||||
properties are read at the corresponding operation. Callbacks retain `this === option`.
|
||||
The control HTML is captured at registration, as before.
|
||||
|
||||
## Modules
|
||||
|
||||
| File | Responsibility |
|
||||
| --- | --- |
|
||||
| `src/index.ts` | Public factory and historical `.default` factory alias |
|
||||
| `src/controller.ts` | One registration's subscriptions, raw session state, click operation, notices and owned control icon |
|
||||
| `src/sdk.ts` | Framework readiness, shared in-flight script leases, callback preservation and context configuration |
|
||||
| `src/media.ts` | Existing URL/extension MIME rules and SDK media request construction |
|
||||
| `src/types.ts` | Narrow internal SDK, event and callback contracts; not the published declaration entrypoint |
|
||||
| `src/cast.svg` | Original default Cast artwork imported as inline SVG |
|
||||
|
||||
Dependencies point from the factory to the controller, then to the SDK loader and
|
||||
media builder. The loader and media builder do not receive an ArtPlayer instance.
|
||||
Public declarations remain in `types/`; their historical mismatch with async
|
||||
registration is tracked separately by PKG-CAST-04. Internal strict typing does not
|
||||
silently change those declarations.
|
||||
|
||||
## Initialization and ownership
|
||||
|
||||
Each registration has its own session state and subscriptions, even when one
|
||||
factory is reused for several players. Unclicked instances do not subscribe to
|
||||
the SDK. An initialized instance observes page-wide CastContext events; this is
|
||||
the SDK's shared session, not an exclusively owned receiver session.
|
||||
|
||||
Within a loaded plugin module, controllers share one pending SDK load per window.
|
||||
The first pending request's SDK URL wins; another URL can be used on a later retry.
|
||||
A complete existing Framework is reused immediately. A partial `chrome.cast`
|
||||
object is insufficient readiness and still permits loading the Framework script.
|
||||
The original availability callback is invoked with its window receiver and both
|
||||
arguments. Its thrown exception still escapes to its caller, while the plugin's
|
||||
own subscribers settle independently. Cleanup restores that handler only while
|
||||
our handler is still installed, so a later external replacement is preserved.
|
||||
|
||||
On availability, each subscriber installs its listeners synchronously before its
|
||||
loader promise resolves. This preserves the historical boundary where a session
|
||||
event immediately following `__onGCastApiAvailable(true)` is observable. One
|
||||
subscriber's setup exception rejects that subscriber without stranding the rest.
|
||||
The default receiver and ORIGIN_SCOPED policy are configured once per observed
|
||||
context. This does not negotiate receiver configuration with unrelated Cast apps.
|
||||
|
||||
Successful scripts remain on the page. Failed or last-owner-cancelled scripts are
|
||||
removed and can be retried. Script `load` may precede Framework availability, so
|
||||
incomplete load does not fail immediately; a 30-second readiness timeout settles
|
||||
an unavailable Framework even if no error/availability event arrives. The timeout
|
||||
is cleared on success, failure and last-owner cancellation. Destroying one player
|
||||
releases its lease without interrupting another pending player. Independently
|
||||
loaded copies of this plugin do not share their module-local lease registry.
|
||||
|
||||
## Session operations and failure semantics
|
||||
|
||||
One player has at most one pending click operation; concurrent clicks receive the
|
||||
same promise. After initialization, the controller reads `getCurrentSession()`.
|
||||
If absent, it awaits `requestSession()` and reads `getCurrentSession()` again.
|
||||
The request's completion value is not a CastSession. No active session after a
|
||||
successful request is a connection error. Media URL selection uses the current
|
||||
`option.url || art.option.url`; no receiver URL rewriting is performed.
|
||||
|
||||
The complete media load is awaited. The click promise rejects with the original
|
||||
SDK error and reports one stage-specific notice and `onError`. A separate rejection
|
||||
observer handles ArtPlayer's DOM dispatcher, which ignores returned click promises;
|
||||
programmatic callers still receive the original rejecting promise. Callback
|
||||
exceptions retain their normal propagation and are not interpreted as receiver
|
||||
success evidence.
|
||||
|
||||
`getCastState()` retains the last raw SDK SessionState, initially null, and is not
|
||||
the normalized callback state. `isCasting()` reflects a retained session reference,
|
||||
including one acquired by a successful session request; it does not prove receiver
|
||||
playback. `NO_SESSION`, `SESSION_ENDED` and `SESSION_START_FAILED` clear that reference
|
||||
and notify disconnected. Initial `NO_SESSION` during SDK setup does not cancel the
|
||||
first click before a session operation begins. Ending/terminal events during a
|
||||
request or media load, and replacement of the current session, invalidate pending
|
||||
work. Late success/rejection is observed without stale media starts or notices.
|
||||
|
||||
Destroy marks the controller inactive, settles its pending click, releases its
|
||||
loader lease, attempts both SDK listener removals and removes its ArtPlayer
|
||||
subscription. All cleanup is attempted even when one SDK removal throws; the first
|
||||
cleanup error is preserved. Any callback left behind by a broken SDK removal is
|
||||
inactive. The plugin never ends a page-shared session. ArtPlayer owns control DOM
|
||||
removal; the plugin only updates the icon under the element supplied to its own
|
||||
control's `mounted` callback, retaining `.art-icon.art-icon-cast` and the
|
||||
red/orange/white states. The actual `Control.add` override returns undefined even
|
||||
though the shared component superclass returns an element; never rely on the
|
||||
control add return value for DOM ownership. Candidate fixtures replay `mounted`
|
||||
and preserve this undefined return so the browser-discovered regression remains
|
||||
covered outside the browser too.
|
||||
|
||||
## Artwork provenance
|
||||
|
||||
The default SVG now uses original rectangle, circular arc and dot geometry authored
|
||||
for this refactor under the repository MIT license. It does not copy the former
|
||||
Font Awesome Pro path. `src/cast.svg` is the single artwork source; custom `icon`
|
||||
HTML and the existing wrapper hooks remain supported. The previous commercial
|
||||
notice/source gap is tracked as VENDOR-10; production bundles must be regenerated
|
||||
from this source rather than edited by hand.
|
||||
|
||||
## Validation and next changes
|
||||
|
||||
Use the pinned Node and Yarn versions. Scoped commands from the repository root:
|
||||
|
||||
```sh
|
||||
node --test test/chromecast.test.js test/chromecast-failures.test.js test/chromecast-runtime.test.js
|
||||
yarn exec tsc -p packages/artplayer-plugin-chromecast/tsconfig.json
|
||||
yarn exec eslint packages/artplayer-plugin-chromecast/src test/chromecast-runtime.test.js
|
||||
yarn build artplayer-plugin-chromecast
|
||||
```
|
||||
|
||||
`ARTPLAYER_CHROMECAST_ARTIFACT` selects a built main or legacy file for the candidate
|
||||
tests. `ARTPLAYER_CHROMECAST_ESM_ARTIFACT` selects the native ESM artifact for the
|
||||
default-export identity check. Without these variables tests bundle current source
|
||||
in memory. Historical tests retain actual frozen npm/source inputs and deliberately
|
||||
observe the old defects; candidate tests assert their corrections separately.
|
||||
|
||||
Change SDK loading in `sdk.ts`, state/cleanup in `controller.ts`, and MIME behavior
|
||||
in `media.ts`. Always retain synchronous availability-event coverage, ignored DOM
|
||||
promise rejection coverage, multi-player ownership, partial failure cleanup and
|
||||
late completion tests when changing those boundaries.
|
||||
|
||||
Controlled SDK tests and local browser DOM interaction cannot establish real Cast
|
||||
receiver playback. PKG-CAST-05 still requires the supported Chrome/HTTPS sender,
|
||||
actual receiver, network-reachable media, source changes and disconnection evidence.
|
||||
The default SDK URL is mutable; no remote SDK version, physical receiver or Safari
|
||||
support is established by this source migration.
|
||||
@@ -4,4 +4,4 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(t.artplayerPluginChromecast=t(),define(function(){return t.artplayerPluginChromecast})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginChromecast=t()}(this,function(){"use strict";return function(e){let t=!1,o=null,n=null;const a=e=>{const t=document.querySelector(".art-icon-cast");if(t)switch(e){case"connected":t.style.color="red";break;case"connecting":case"disconnecting":t.style.color="orange";break;default:t.style.color="white"}},c=()=>new Promise((c,s)=>{var i;window.__onGCastApiAvailable=i=>{if(i){const s=window.cast.framework.CastContext.getInstance();s.setOptions({receiverApplicationId:window.chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID,autoJoinPolicy:window.chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED}),s.addEventListener(window.cast.framework.CastContextEventType.SESSION_STATE_CHANGED,t=>{const c=window.cast.framework.SessionState;switch(n=t.sessionState,o=t.session,t.sessionState){case c.NO_SESSION:e.onStateChange?.("disconnected"),a("disconnected");break;case c.SESSION_STARTING:e.onStateChange?.("connecting"),a("connecting");break;case c.SESSION_STARTED:e.onStateChange?.("connected"),a("connected");break;case c.SESSION_ENDING:e.onStateChange?.("disconnecting"),a("disconnecting");break;case c.SESSION_RESUMED:e.onStateChange?.("connected"),a("connected")}}),s.addEventListener(window.cast.framework.CastContextEventType.CAST_STATE_CHANGED,t=>{const o=window.cast.framework.CastState;switch(t.castState){case o.NO_DEVICES_AVAILABLE:e.onCastAvailable?.(!1);break;case o.NOT_CONNECTED:case o.CONNECTING:case o.CONNECTED:e.onCastAvailable?.(!0)}}),t=!0,c()}else s(new Error("Cast API is not available"))},window.chrome&&window.chrome.cast||(i=e.sdk||"https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1",new Promise((e,t)=>{const o=document.createElement("script");o.src=i,o.onload=e,o.onerror=t,document.body.appendChild(o)})).catch(s)}),s=(t,o)=>{const n=e.url||t.option.url,a=new window.chrome.cast.media.MediaInfo(n,e.mimeType||function(e){return{mp4:"video/mp4",webm:"video/webm",ogg:"video/ogg",ogv:"video/ogg",mp3:"audio/mp3",wav:"audio/wav",flv:"video/x-flv",mov:"video/quicktime",avi:"video/x-msvideo",wmv:"video/x-ms-wmv",mpd:"application/dash+xml",m3u8:"application/x-mpegURL"}[e.split("?")[0].split("#")[0].split(".").pop().toLowerCase()]||"application/octet-stream"}(n)),c=new window.chrome.cast.media.LoadRequest(a);o.loadMedia(c).then(()=>{t.notice.show="Casting started",e.onCastStart?.()}).catch(o=>{throw t.notice.show="Error casting media",e.onError?.(o),o})};return async a=>(a.controls.add({name:"chromecast",position:"right",tooltip:"Chromecast",html:`<i class="art-icon art-icon-cast">${e.icon||'<svg height="20" width="20" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M512 96H64v99c-13-2-26.4-3-40-3H0V96C0 60.7 28.7 32 64 32H512c35.3 0 64 28.7 64 64V416c0 35.3-28.7 64-64 64H288V456c0-13.6-1-27-3-40H512V96zM24 224c128.1 0 232 103.9 232 232c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-101.6-82.4-184-184-184c-13.3 0-24-10.7-24-24s10.7-24 24-24zm8 192a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM0 344c0-13.3 10.7-24 24-24c75.1 0 136 60.9 136 136c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-48.6-39.4-88-88-88c-13.3 0-24-10.7-24-24z"/></svg>'}</i>`,click:async()=>{if(!t)try{await c()}catch(i){throw a.notice.show="Failed to initialize Cast API",e.onError?.(i),i}const n=window.cast.framework.CastContext.getInstance();if(o)s(a,o);else try{const e=await n.requestSession();s(a,e)}catch(i){throw a.notice.show="Error connecting to cast session",e.onError?.(i),i}}}),{name:"artplayerPluginChromecast",getCastState:()=>n,isCasting:()=>null!==o})}});
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginChromecast=t(),define(function(){return e.artplayerPluginChromecast})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginChromecast=t()}(this,function(){"use strict";const e={mp4:"video/mp4",webm:"video/webm",ogg:"video/ogg",ogv:"video/ogg",mp3:"audio/mp3",wav:"audio/wav",flv:"video/x-flv",mov:"video/quicktime",avi:"video/x-msvideo",wmv:"video/x-ms-wmv",mpd:"application/dash+xml",m3u8:"application/x-mpegURL"};function t(t,o,n,i){const r=n.url||i,s=r.split("?")[0].split("#")[0].split(".").pop().toLowerCase(),a=new t.media.MediaInfo(r,n.mimeType||e[s]||"application/octet-stream");return o.loadMedia(new t.media.LoadRequest(a))}const o=new WeakMap,n=new WeakSet;function i(e){const t=e.cast?.framework,o=e.chrome?.cast;if("function"===typeof t?.CastContext?.getInstance&&t.CastContextEventType&&t.SessionState&&t.CastState&&"function"===typeof o?.media?.MediaInfo&&"function"===typeof o.media.LoadRequest&&o.AutoJoinPolicy)return{framework:t,media:o.media,autoJoinPolicy:o.AutoJoinPolicy.ORIGIN_SCOPED}}function r(e,t){let o;const n=new Promise((n,i)=>{o={ready:t,resolve:n,reject:i},e.subscribers.add(o)});let i=!1;return{promise:n,release:()=>{i||(i=!0,e.subscribers.delete(o),o.reject(new Error("Cast initialization cancelled")),e.subscribers.size||e.cancel())}}}function s(e,s){let a,c,l,d,u,f,m=!1,v=null,S=null,p=0,E=!1;const w=new Promise(e=>{f=()=>e(void 0)});let C;const h=()=>!m&&!e.isDestroy,y=e=>{if(s.onStateChange?.(e),!h())return;const t=C?.querySelector(".art-icon-cast");t&&(t.style.color="connected"===e?"red":"connecting"===e||"disconnecting"===e?"orange":"white")},g=e=>{if(!h()||!a)return;S=e.sessionState;const t=a.framework.SessionState;S===t.NO_SESSION||S===t.SESSION_ENDED||S===t.SESSION_START_FAILED?((S!==t.NO_SESSION||E)&&(p++,u?.()),v=null,y("disconnected")):((S===t.SESSION_ENDING||v&&e.session&&e.session!==v)&&(p++,u?.()),v=e.session||null,S===t.SESSION_STARTING?y("connecting"):S===t.SESSION_ENDING?y("disconnecting"):S!==t.SESSION_STARTED&&S!==t.SESSION_RESUMED||y("connected"))},A=e=>{if(!h()||!a)return;const t=a.framework.CastState;e.castState===t.NO_DEVICES_AVAILABLE?s.onCastAvailable?.(!1):e.castState!==t.NOT_CONNECTED&&e.castState!==t.CONNECTING&&e.castState!==t.CONNECTED||s.onCastAvailable?.(!0)},_=()=>{const e=c,t=a;if(c=void 0,a=void 0,e&&t){const n=t.framework.CastContextEventType,i=[];try{e.removeEventListener(n.SESSION_STATE_CHANGED,g)}catch(o){i.push(o)}try{e.removeEventListener(n.CAST_STATE_CHANGED,A)}catch(o){i.push(o)}if(i.length)throw i[0]}},N=()=>{if(m)return;m=!0,f();const t=d;d=void 0;const o=[];for(const i of[t,_,()=>e.off("destroy",N)])try{i?.()}catch(n){o.push(n)}if(v=null,C=void 0,o.length)throw o[0]};async function I(){const l=p,f=()=>h()&&p===l;let m;const S=new Promise(e=>{m=()=>e(void 0)});u=m;let C="Failed to initialize Cast API";try{if(!a||!c){const e=function(e,t){const n=window,s=i(n);if(s)return{promise:new Promise((e,o)=>{try{t(s),e(s)}catch(n){o(n)}}),release:()=>{}};const a=o.get(n);if(a)return r(a,t);const c={subscribers:new Set,cancel:()=>v(void 0,new Error("Cast initialization cancelled"))};o.set(n,c);const l=r(c,t),d=n.__onGCastApiAvailable;let u,f,m=!1;function v(e,t){if(!m){m=!0,void 0!==f&&n.clearTimeout(f),o.delete(n),n.__onGCastApiAvailable===S&&(n.__onGCastApiAvailable=d),u&&(u.onload=null,u.onerror=null,e||u.remove());for(const o of c.subscribers)try{e?(o.ready(e),o.resolve(e)):o.reject(t)}catch(i){o.reject(i)}c.subscribers.clear()}}function S(e,t){try{d?.call(n,e,t)}finally{v(e?i(n):void 0,new Error("Cast API is not available"))}}n.__onGCastApiAvailable=S;try{f=n.setTimeout(()=>v(void 0,new Error("Timed out initializing Cast API")),3e4),u=document.createElement("script"),u.src=e||"https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1",u.onerror=e=>v(void 0,e),u.onload=()=>{const e=i(n);e&&v(e)},document.body.appendChild(u)}catch(p){v(void 0,p)}return l}(s.sdk,e=>{if(!f())return;if(a=e,c=function(e){const t=e.framework.CastContext.getInstance();return n.has(t)||(t.setOptions({receiverApplicationId:e.media.DEFAULT_MEDIA_RECEIVER_APP_ID,autoJoinPolicy:e.autoJoinPolicy}),n.add(t)),t}(a),!f())return;const t=a.framework.CastContextEventType;try{c.addEventListener(t.SESSION_STATE_CHANGED,g),h()&&c.addEventListener(t.CAST_STATE_CHANGED,A)}catch(o){try{_()}catch{}throw o}});d=e.release,await Promise.race([e.promise,w,S]),e.release(),d=void 0}if(!f()||!c||!a)return;C="Error connecting to cast session",E=!0;let l=c.getCurrentSession();if(!f())return;if(!l){if(await Promise.race([c.requestSession(),w,S]),!f())return;l=c.getCurrentSession()}if(!f())return;if(!l)throw new Error("No active Cast session");v=l,C="Error casting media",await Promise.race([t(a,l,s,e.option.url),w,S]),f()&&(e.notice.show="Casting started",s.onCastStart?.())}catch(y){if(f())throw e.notice.show=C,s.onError?.(y),y}Line truncated
|
||||
+1
-1
@@ -4,4 +4,4 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(t.artplayerPluginChromecast=t(),define(function(){return t.artplayerPluginChromecast})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginChromecast=t()}(this,function(){"use strict";var e=(e,t,n)=>new Promise((o,a)=>{var c=e=>{try{i(n.next(e))}catch(t){a(t)}},s=e=>{try{i(n.throw(e))}catch(t){a(t)}},i=e=>e.done?o(e.value):Promise.resolve(e.value).then(c,s);i((n=n.apply(e,t)).next())});return function(t){let n=!1,o=null,a=null;const c=e=>{const t=document.querySelector(".art-icon-cast");if(t)switch(e){case"connected":t.style.color="red";break;case"connecting":case"disconnecting":t.style.color="orange";break;default:t.style.color="white"}},s=()=>new Promise((e,s)=>{var i;window.__onGCastApiAvailable=i=>{if(i){const s=window.cast.framework.CastContext.getInstance();s.setOptions({receiverApplicationId:window.chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID,autoJoinPolicy:window.chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED}),s.addEventListener(window.cast.framework.CastContextEventType.SESSION_STATE_CHANGED,e=>{var n,s,i,l,r;const d=window.cast.framework.SessionState;switch(a=e.sessionState,o=e.session,e.sessionState){case d.NO_SESSION:null==(n=t.onStateChange)||n.call(t,"disconnected"),c("disconnected");break;case d.SESSION_STARTING:null==(s=t.onStateChange)||s.call(t,"connecting"),c("connecting");break;case d.SESSION_STARTED:null==(i=t.onStateChange)||i.call(t,"connected"),c("connected");break;case d.SESSION_ENDING:null==(l=t.onStateChange)||l.call(t,"disconnecting"),c("disconnecting");break;case d.SESSION_RESUMED:null==(r=t.onStateChange)||r.call(t,"connected"),c("connected")}}),s.addEventListener(window.cast.framework.CastContextEventType.CAST_STATE_CHANGED,e=>{var n,o,a;const c=window.cast.framework.CastState;switch(e.castState){case c.NO_DEVICES_AVAILABLE:null==(n=t.onCastAvailable)||n.call(t,!1);break;case c.NOT_CONNECTED:null==(o=t.onCastAvailable)||o.call(t,!0);break;case c.CONNECTING:case c.CONNECTED:null==(a=t.onCastAvailable)||a.call(t,!0)}}),n=!0,e()}else s(new Error("Cast API is not available"))},window.chrome&&window.chrome.cast||(i=t.sdk||"https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1",new Promise((e,t)=>{const n=document.createElement("script");n.src=i,n.onload=e,n.onerror=t,document.body.appendChild(n)})).catch(s)}),i=(e,n)=>{const o=t.url||e.option.url,a=new window.chrome.cast.media.MediaInfo(o,t.mimeType||function(e){return{mp4:"video/mp4",webm:"video/webm",ogg:"video/ogg",ogv:"video/ogg",mp3:"audio/mp3",wav:"audio/wav",flv:"video/x-flv",mov:"video/quicktime",avi:"video/x-msvideo",wmv:"video/x-ms-wmv",mpd:"application/dash+xml",m3u8:"application/x-mpegURL"}[e.split("?")[0].split("#")[0].split(".").pop().toLowerCase()]||"application/octet-stream"}(o)),c=new window.chrome.cast.media.LoadRequest(a);n.loadMedia(c).then(()=>{var n;e.notice.show="Casting started",null==(n=t.onCastStart)||n.call(t)}).catch(n=>{var o;throw e.notice.show="Error casting media",null==(o=t.onError)||o.call(t,n),n})};return c=>e(null,null,function*(){return c.controls.add({name:"chromecast",position:"right",tooltip:"Chromecast",html:`<i class="art-icon art-icon-cast">${t.icon||'<svg height="20" width="20" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M512 96H64v99c-13-2-26.4-3-40-3H0V96C0 60.7 28.7 32 64 32H512c35.3 0 64 28.7 64 64V416c0 35.3-28.7 64-64 64H288V456c0-13.6-1-27-3-40H512V96zM24 224c128.1 0 232 103.9 232 232c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-101.6-82.4-184-184-184c-13.3 0-24-10.7-24-24s10.7-24 24-24zm8 192a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM0 344c0-13.3 10.7-24 24-24c75.1 0 136 60.9 136 136c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-48.6-39.4-88-88-88c-13.3 0-24-10.7-24-24z"/></svg>'}</i>`,click:()=>e(null,null,function*(){var e,a;if(!n)try{yield s()}catch(r){throw c.notice.show="Failed to initialize Cast API",null==(e=t.onError)||e.call(t,r),r}const l=window.cast.framework.CastContext.getInstance();if(o)i(c,o);else try{const e=yield l.requestSession();i(c,e)}catch(r){throw c.notice.show="Error connecting to cast session",null==(a=t.onError)||a.call(t,r),r}})}),{name:"artplayerPluginChromecast",getCastState:()=>a,isCasting:()=>null!==o}})}});
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginChromecast=t(),define(function(){return e.artplayerPluginChromecast})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginChromecast=t()}(this,function(){"use strict";var e=(e,t,o)=>new Promise((n,i)=>{var r=e=>{try{a(o.next(e))}catch(t){i(t)}},s=e=>{try{a(o.throw(e))}catch(t){i(t)}},a=e=>e.done?n(e.value):Promise.resolve(e.value).then(r,s);a((o=o.apply(e,t)).next())});const t={mp4:"video/mp4",webm:"video/webm",ogg:"video/ogg",ogv:"video/ogg",mp3:"audio/mp3",wav:"audio/wav",flv:"video/x-flv",mov:"video/quicktime",avi:"video/x-msvideo",wmv:"video/x-ms-wmv",mpd:"application/dash+xml",m3u8:"application/x-mpegURL"};function o(e,o,n,i){const r=n.url||i,s=r.split("?")[0].split("#")[0].split(".").pop().toLowerCase(),a=new e.media.MediaInfo(r,n.mimeType||t[s]||"application/octet-stream");return o.loadMedia(new e.media.LoadRequest(a))}const n=new WeakMap,i=new WeakSet;function r(e){var t,o,n,i;const r=null==(t=e.cast)?void 0:t.framework,s=null==(o=e.chrome)?void 0:o.cast;if("function"===typeof(null==(n=null==r?void 0:r.CastContext)?void 0:n.getInstance)&&r.CastContextEventType&&r.SessionState&&r.CastState&&"function"===typeof(null==(i=null==s?void 0:s.media)?void 0:i.MediaInfo)&&"function"===typeof s.media.LoadRequest&&s.AutoJoinPolicy)return{framework:r,media:s.media,autoJoinPolicy:s.AutoJoinPolicy.ORIGIN_SCOPED}}function s(e,t){let o;const n=new Promise((n,i)=>{o={ready:t,resolve:n,reject:i},e.subscribers.add(o)});let i=!1;return{promise:n,release:()=>{i||(i=!0,e.subscribers.delete(o),o.reject(new Error("Cast initialization cancelled")),e.subscribers.size||e.cancel())}}}function a(t,a){let l,c,u,d,v,f,m=!1,S=null,p=null,E=0,C=!1;const w=new Promise(e=>{f=()=>e(void 0)});let h;const y=()=>!m&&!t.isDestroy,g=e=>{var t;if(null==(t=a.onStateChange)||t.call(a,e),!y())return;const o=null==h?void 0:h.querySelector(".art-icon-cast");o&&(o.style.color="connected"===e?"red":"connecting"===e||"disconnecting"===e?"orange":"white")},A=e=>{if(!y()||!l)return;p=e.sessionState;const t=l.framework.SessionState;p===t.NO_SESSION||p===t.SESSION_ENDED||p===t.SESSION_START_FAILED?((p!==t.NO_SESSION||C)&&(E++,null==v||v()),S=null,g("disconnected")):((p===t.SESSION_ENDING||S&&e.session&&e.session!==S)&&(E++,null==v||v()),S=e.session||null,p===t.SESSION_STARTING?g("connecting"):p===t.SESSION_ENDING?g("disconnecting"):p!==t.SESSION_STARTED&&p!==t.SESSION_RESUMED||g("connected"))},_=e=>{var t,o;if(!y()||!l)return;const n=l.framework.CastState;e.castState===n.NO_DEVICES_AVAILABLE?null==(t=a.onCastAvailable)||t.call(a,!1):e.castState!==n.NOT_CONNECTED&&e.castState!==n.CONNECTING&&e.castState!==n.CONNECTED||null==(o=a.onCastAvailable)||o.call(a,!0)},N=()=>{const e=c,t=l;if(c=void 0,l=void 0,e&&t){const n=t.framework.CastContextEventType,i=[];try{e.removeEventListener(n.SESSION_STATE_CHANGED,A)}catch(o){i.push(o)}try{e.removeEventListener(n.CAST_STATE_CHANGED,_)}catch(o){i.push(o)}if(i.length)throw i[0]}},I=()=>{if(m)return;m=!0,f();const e=d;d=void 0;const o=[];for(const i of[e,N,()=>t.off("destroy",I)])try{null==i||i()}catch(n){o.push(n)}if(S=null,h=void 0,o.length)throw o[0]};function T(){return e(this,null,function*(){var e,u;const f=E,m=()=>y()&&E===f;let p;const h=new Promise(e=>{p=()=>e(void 0)});v=p;let g="Failed to initialize Cast API";try{if(!l||!c){const e=function(e,t){const o=window,i=r(o);if(i)return{promise:new Promise((e,o)=>{try{t(i),e(i)}catch(n){o(n)}}),release:()=>{}};const a=n.get(o);if(a)return s(a,t);const l={subscribers:new Set,cancel:()=>m(void 0,new Error("Cast initialization cancelled"))};n.set(o,l);const c=s(l,t),u=o.__onGCastApiAvailable;let d,v,f=!1;function m(e,t){if(!f){f=!0,void 0!==v&&o.clearTimeout(v),n.delete(o),o.__onGCastApiAvailable===S&&(o.__onGCastApiAvailable=u),d&&(d.onload=null,d.onerror=null,e||d.remove());for(const o of l.subscribers)try{e?(o.ready(e),o.resolve(e)):o.reject(t)}catch(i){o.reject(i)}l.subscribers.clear()}}function S(e,t){try{null==u||u.call(o,e,t)}finally{m(e?r(o):void 0,new Error("Cast API is not available"))}}o.__onGCastApiAvailable=S;try{v=o.setTimeout(()=>m(void 0,new Error("Timed out initializing Cast API")),3e4),d=document.createElement("script"),d.src=e||"https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1",d.onerror=e=>m(void 0,e),d.onload=()=>{const e=r(o);e&&m(e)},document.body.appendChild(d)}catch(p){m(void 0,p)}return c}(a.sdk,e=>{if(!m())return;if(l=e,c=function(e){const t=e.framework.CastContext.getInstance();return i.has(t)||(t.setOptions({receiverApplicationId:e.media.DEFAULT_MEDIA_RECEIVER_APP_ID,autoJoinPolicy:e.autoJoinPolicy}),i.add(t)),t}(l),!m())return;const t=l.framework.CastContextEventType;try{c.addEventListener(t.SESSION_STATE_CHANGED,A),y()&&c.addEventListener(t.CAST_STATE_CHANGED,_)}catch(o){try{N()}catch(n){}throw o}});d=e.release,yield Promise.race([e.promise,w,h]),e.relLine truncated
|
||||
+347
-161
@@ -4,173 +4,359 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
function loadScript(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = document.createElement("script");
|
||||
script.src = src;
|
||||
script.onload = resolve;
|
||||
script.onerror = reject;
|
||||
document.body.appendChild(script);
|
||||
});
|
||||
}
|
||||
function getMimeType(url) {
|
||||
const icon = '<svg height="20" width="20" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7V4h17v14h-8M3 11a10 10 0 0 1 10 10M3 16a5 5 0 0 1 5 5"/><circle cx="3" cy="21" r="1" fill="currentColor" stroke="none"/></svg>\r\n';
|
||||
const mimeTypes = {
|
||||
mp4: "video/mp4",
|
||||
webm: "video/webm",
|
||||
ogg: "video/ogg",
|
||||
ogv: "video/ogg",
|
||||
mp3: "audio/mp3",
|
||||
wav: "audio/wav",
|
||||
flv: "video/x-flv",
|
||||
mov: "video/quicktime",
|
||||
avi: "video/x-msvideo",
|
||||
wmv: "video/x-ms-wmv",
|
||||
mpd: "application/dash+xml",
|
||||
m3u8: "application/x-mpegURL"
|
||||
};
|
||||
function loadMedia(sdk, session, option, currentUrl) {
|
||||
const url = option.url || currentUrl;
|
||||
const extension = url.split("?")[0].split("#")[0].split(".").pop().toLowerCase();
|
||||
const mimeTypes = {
|
||||
mp4: "video/mp4",
|
||||
webm: "video/webm",
|
||||
ogg: "video/ogg",
|
||||
ogv: "video/ogg",
|
||||
mp3: "audio/mp3",
|
||||
wav: "audio/wav",
|
||||
flv: "video/x-flv",
|
||||
mov: "video/quicktime",
|
||||
avi: "video/x-msvideo",
|
||||
wmv: "video/x-ms-wmv",
|
||||
mpd: "application/dash+xml",
|
||||
m3u8: "application/x-mpegURL"
|
||||
};
|
||||
return mimeTypes[extension] || "application/octet-stream";
|
||||
const info = new sdk.media.MediaInfo(url, option.mimeType || mimeTypes[extension] || "application/octet-stream");
|
||||
return session.loadMedia(new sdk.media.LoadRequest(info));
|
||||
}
|
||||
function artplayerPluginChromecast(option) {
|
||||
const DEFAULT_ICON = `<svg height="20" width="20" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M512 96H64v99c-13-2-26.4-3-40-3H0V96C0 60.7 28.7 32 64 32H512c35.3 0 64 28.7 64 64V416c0 35.3-28.7 64-64 64H288V456c0-13.6-1-27-3-40H512V96zM24 224c128.1 0 232 103.9 232 232c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-101.6-82.4-184-184-184c-13.3 0-24-10.7-24-24s10.7-24 24-24zm8 192a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM0 344c0-13.3 10.7-24 24-24c75.1 0 136 60.9 136 136c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-48.6-39.4-88-88-88c-13.3 0-24-10.7-24-24z"/></svg>`;
|
||||
const DEFAULT_SDK = "https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1";
|
||||
let isCastInitialized = false;
|
||||
let castSession = null;
|
||||
let castState = null;
|
||||
const updateCastButton = (state) => {
|
||||
const button = document.querySelector(".art-icon-cast");
|
||||
if (button) {
|
||||
switch (state) {
|
||||
case "connected":
|
||||
button.style.color = "red";
|
||||
break;
|
||||
case "connecting":
|
||||
case "disconnecting":
|
||||
button.style.color = "orange";
|
||||
break;
|
||||
case "disconnected":
|
||||
default:
|
||||
button.style.color = "white";
|
||||
break;
|
||||
const DEFAULT_SDK = "https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1";
|
||||
const pending = /* @__PURE__ */ new WeakMap();
|
||||
const configured = /* @__PURE__ */ new WeakSet();
|
||||
function ready(host) {
|
||||
const framework = host.cast?.framework;
|
||||
const api = host.chrome?.cast;
|
||||
if (typeof framework?.CastContext?.getInstance === "function" && framework.CastContextEventType && framework.SessionState && framework.CastState && typeof api?.media?.MediaInfo === "function" && typeof api.media.LoadRequest === "function" && api.AutoJoinPolicy) {
|
||||
return { framework, media: api.media, autoJoinPolicy: api.AutoJoinPolicy.ORIGIN_SCOPED };
|
||||
}
|
||||
}
|
||||
function configure(sdk) {
|
||||
const context = sdk.framework.CastContext.getInstance();
|
||||
if (!configured.has(context)) {
|
||||
context.setOptions({ receiverApplicationId: sdk.media.DEFAULT_MEDIA_RECEIVER_APP_ID, autoJoinPolicy: sdk.autoJoinPolicy });
|
||||
configured.add(context);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
function lease(loading, onReady) {
|
||||
let subscriber;
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
subscriber = { ready: onReady, resolve, reject };
|
||||
loading.subscribers.add(subscriber);
|
||||
});
|
||||
let released = false;
|
||||
return { promise, release: () => {
|
||||
if (released)
|
||||
return;
|
||||
released = true;
|
||||
loading.subscribers.delete(subscriber);
|
||||
subscriber.reject(new Error("Cast initialization cancelled"));
|
||||
if (!loading.subscribers.size)
|
||||
loading.cancel();
|
||||
} };
|
||||
}
|
||||
function loadSdk(src, onReady) {
|
||||
const host = window;
|
||||
const loaded = ready(host);
|
||||
if (loaded) {
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
try {
|
||||
onReady(loaded);
|
||||
resolve(loaded);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
return { promise, release: () => {
|
||||
} };
|
||||
}
|
||||
const existing = pending.get(host);
|
||||
if (existing)
|
||||
return lease(existing, onReady);
|
||||
const loading = { subscribers: /* @__PURE__ */ new Set(), cancel: () => finish(void 0, new Error("Cast initialization cancelled")) };
|
||||
pending.set(host, loading);
|
||||
const subscription = lease(loading, onReady);
|
||||
const previous = host.__onGCastApiAvailable;
|
||||
let script;
|
||||
let timer;
|
||||
let settled = false;
|
||||
function finish(sdk, error) {
|
||||
if (settled)
|
||||
return;
|
||||
settled = true;
|
||||
if (timer !== void 0)
|
||||
host.clearTimeout(timer);
|
||||
pending.delete(host);
|
||||
if (host.__onGCastApiAvailable === available)
|
||||
host.__onGCastApiAvailable = previous;
|
||||
if (script) {
|
||||
script.onload = null;
|
||||
script.onerror = null;
|
||||
if (!sdk)
|
||||
script.remove();
|
||||
}
|
||||
for (const subscriber of loading.subscribers) {
|
||||
try {
|
||||
if (sdk) {
|
||||
subscriber.ready(sdk);
|
||||
subscriber.resolve(sdk);
|
||||
} else {
|
||||
subscriber.reject(error);
|
||||
}
|
||||
} catch (error2) {
|
||||
subscriber.reject(error2);
|
||||
}
|
||||
}
|
||||
};
|
||||
const initializeCastApi = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
window.__onGCastApiAvailable = (isAvailable) => {
|
||||
if (isAvailable) {
|
||||
const context = window.cast.framework.CastContext.getInstance();
|
||||
context.setOptions({
|
||||
receiverApplicationId: window.chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID,
|
||||
autoJoinPolicy: window.chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED
|
||||
});
|
||||
context.addEventListener(
|
||||
window.cast.framework.CastContextEventType.SESSION_STATE_CHANGED,
|
||||
(event) => {
|
||||
const SessionState = window.cast.framework.SessionState;
|
||||
castState = event.sessionState;
|
||||
castSession = event.session;
|
||||
switch (event.sessionState) {
|
||||
case SessionState.NO_SESSION:
|
||||
option.onStateChange?.("disconnected");
|
||||
updateCastButton("disconnected");
|
||||
break;
|
||||
case SessionState.SESSION_STARTING:
|
||||
option.onStateChange?.("connecting");
|
||||
updateCastButton("connecting");
|
||||
break;
|
||||
case SessionState.SESSION_STARTED:
|
||||
option.onStateChange?.("connected");
|
||||
updateCastButton("connected");
|
||||
break;
|
||||
case SessionState.SESSION_ENDING:
|
||||
option.onStateChange?.("disconnecting");
|
||||
updateCastButton("disconnecting");
|
||||
break;
|
||||
case SessionState.SESSION_RESUMED:
|
||||
option.onStateChange?.("connected");
|
||||
updateCastButton("connected");
|
||||
break;
|
||||
}
|
||||
}
|
||||
);
|
||||
context.addEventListener(window.cast.framework.CastContextEventType.CAST_STATE_CHANGED, (event) => {
|
||||
const CastState = window.cast.framework.CastState;
|
||||
switch (event.castState) {
|
||||
case CastState.NO_DEVICES_AVAILABLE:
|
||||
option.onCastAvailable?.(false);
|
||||
break;
|
||||
case CastState.NOT_CONNECTED:
|
||||
option.onCastAvailable?.(true);
|
||||
break;
|
||||
case CastState.CONNECTING:
|
||||
case CastState.CONNECTED:
|
||||
option.onCastAvailable?.(true);
|
||||
break;
|
||||
}
|
||||
});
|
||||
isCastInitialized = true;
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error("Cast API is not available"));
|
||||
}
|
||||
};
|
||||
if (!window.chrome || !window.chrome.cast) {
|
||||
loadScript(option.sdk || DEFAULT_SDK).catch(reject);
|
||||
}
|
||||
});
|
||||
};
|
||||
const castVideo = (art, session) => {
|
||||
const url = option.url || art.option.url;
|
||||
const mediaInfo = new window.chrome.cast.media.MediaInfo(url, option.mimeType || getMimeType(url));
|
||||
const request = new window.chrome.cast.media.LoadRequest(mediaInfo);
|
||||
session.loadMedia(request).then(() => {
|
||||
art.notice.show = "Casting started";
|
||||
option.onCastStart?.();
|
||||
}).catch((error) => {
|
||||
art.notice.show = "Error casting media";
|
||||
option.onError?.(error);
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
return async (art) => {
|
||||
art.controls.add({
|
||||
name: "chromecast",
|
||||
position: "right",
|
||||
tooltip: "Chromecast",
|
||||
html: `<i class="art-icon art-icon-cast">${option.icon || DEFAULT_ICON}</i>`,
|
||||
click: async () => {
|
||||
if (!isCastInitialized) {
|
||||
try {
|
||||
await initializeCastApi();
|
||||
} catch (error) {
|
||||
art.notice.show = "Failed to initialize Cast API";
|
||||
option.onError?.(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
const context = window.cast.framework.CastContext.getInstance();
|
||||
if (castSession) {
|
||||
castVideo(art, castSession);
|
||||
} else {
|
||||
try {
|
||||
const session = await context.requestSession();
|
||||
castVideo(art, session);
|
||||
} catch (error) {
|
||||
art.notice.show = "Error connecting to cast session";
|
||||
option.onError?.(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return {
|
||||
name: "artplayerPluginChromecast",
|
||||
getCastState: () => castState,
|
||||
isCasting: () => castSession !== null
|
||||
loading.subscribers.clear();
|
||||
}
|
||||
function available(value, errorInfo) {
|
||||
try {
|
||||
previous?.call(host, value, errorInfo);
|
||||
} finally {
|
||||
const sdk = value ? ready(host) : void 0;
|
||||
finish(sdk, new Error("Cast API is not available"));
|
||||
}
|
||||
}
|
||||
host.__onGCastApiAvailable = available;
|
||||
try {
|
||||
timer = host.setTimeout(() => finish(void 0, new Error("Timed out initializing Cast API")), 3e4);
|
||||
script = document.createElement("script");
|
||||
script.src = src || DEFAULT_SDK;
|
||||
script.onerror = (error) => finish(void 0, error);
|
||||
script.onload = () => {
|
||||
const sdk = ready(host);
|
||||
if (sdk)
|
||||
finish(sdk);
|
||||
};
|
||||
};
|
||||
document.body.appendChild(script);
|
||||
} catch (error) {
|
||||
finish(void 0, error);
|
||||
}
|
||||
return subscription;
|
||||
}
|
||||
function createController(art, option) {
|
||||
let disposed = false;
|
||||
let sdk;
|
||||
let context;
|
||||
let session = null;
|
||||
let state = null;
|
||||
let activeClick;
|
||||
let releaseLoader;
|
||||
let generation = 0;
|
||||
let operationStarted = false;
|
||||
let cancelOperation;
|
||||
let cancel;
|
||||
const cancelled = new Promise((resolve) => {
|
||||
cancel = () => resolve(void 0);
|
||||
});
|
||||
let element;
|
||||
const active = () => !disposed && !art.isDestroy;
|
||||
const update = (value) => {
|
||||
option.onStateChange?.(value);
|
||||
if (!active())
|
||||
return;
|
||||
const button = element?.querySelector(".art-icon-cast");
|
||||
if (button)
|
||||
button.style.color = value === "connected" ? "red" : value === "connecting" || value === "disconnecting" ? "orange" : "white";
|
||||
};
|
||||
const onSession = (event) => {
|
||||
if (!active() || !sdk)
|
||||
return;
|
||||
state = event.sessionState;
|
||||
const states = sdk.framework.SessionState;
|
||||
if (state === states.NO_SESSION || state === states.SESSION_ENDED || state === states.SESSION_START_FAILED) {
|
||||
if (state !== states.NO_SESSION || operationStarted) {
|
||||
generation++;
|
||||
cancelOperation?.();
|
||||
}
|
||||
session = null;
|
||||
update("disconnected");
|
||||
} else {
|
||||
if (state === states.SESSION_ENDING || session && event.session && event.session !== session) {
|
||||
generation++;
|
||||
cancelOperation?.();
|
||||
}
|
||||
session = event.session || null;
|
||||
if (state === states.SESSION_STARTING)
|
||||
update("connecting");
|
||||
else if (state === states.SESSION_ENDING)
|
||||
update("disconnecting");
|
||||
else if (state === states.SESSION_STARTED || state === states.SESSION_RESUMED)
|
||||
update("connected");
|
||||
}
|
||||
};
|
||||
const onAvailability = (event) => {
|
||||
if (!active() || !sdk)
|
||||
return;
|
||||
const states = sdk.framework.CastState;
|
||||
if (event.castState === states.NO_DEVICES_AVAILABLE)
|
||||
option.onCastAvailable?.(false);
|
||||
else if (event.castState === states.NOT_CONNECTED || event.castState === states.CONNECTING || event.castState === states.CONNECTED)
|
||||
option.onCastAvailable?.(true);
|
||||
};
|
||||
const detach = () => {
|
||||
const ownedContext = context;
|
||||
const ownedSdk = sdk;
|
||||
context = void 0;
|
||||
sdk = void 0;
|
||||
if (ownedContext && ownedSdk) {
|
||||
const events = ownedSdk.framework.CastContextEventType;
|
||||
const errors = [];
|
||||
try {
|
||||
ownedContext.removeEventListener(events.SESSION_STATE_CHANGED, onSession);
|
||||
} catch (error) {
|
||||
errors.push(error);
|
||||
}
|
||||
try {
|
||||
ownedContext.removeEventListener(events.CAST_STATE_CHANGED, onAvailability);
|
||||
} catch (error) {
|
||||
errors.push(error);
|
||||
}
|
||||
if (errors.length)
|
||||
throw errors[0];
|
||||
}
|
||||
};
|
||||
const destroy = () => {
|
||||
if (disposed)
|
||||
return;
|
||||
disposed = true;
|
||||
cancel();
|
||||
const release = releaseLoader;
|
||||
releaseLoader = void 0;
|
||||
const errors = [];
|
||||
for (const cleanup of [release, detach, () => art.off("destroy", destroy)]) {
|
||||
try {
|
||||
cleanup?.();
|
||||
} catch (error) {
|
||||
errors.push(error);
|
||||
}
|
||||
}
|
||||
session = null;
|
||||
element = void 0;
|
||||
if (errors.length)
|
||||
throw errors[0];
|
||||
};
|
||||
async function cast() {
|
||||
const epoch = generation;
|
||||
const currentOperation = () => active() && generation === epoch;
|
||||
let stop;
|
||||
const superseded = new Promise((resolve) => {
|
||||
stop = () => resolve(void 0);
|
||||
});
|
||||
cancelOperation = stop;
|
||||
let failureNotice = "Failed to initialize Cast API";
|
||||
try {
|
||||
if (!sdk || !context) {
|
||||
const loading = loadSdk(option.sdk, (available) => {
|
||||
if (!currentOperation())
|
||||
return;
|
||||
sdk = available;
|
||||
context = configure(sdk);
|
||||
if (!currentOperation())
|
||||
return;
|
||||
const events = sdk.framework.CastContextEventType;
|
||||
try {
|
||||
context.addEventListener(events.SESSION_STATE_CHANGED, onSession);
|
||||
if (active())
|
||||
context.addEventListener(events.CAST_STATE_CHANGED, onAvailability);
|
||||
} catch (error) {
|
||||
try {
|
||||
detach();
|
||||
} catch {
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
releaseLoader = loading.release;
|
||||
await Promise.race([loading.promise, cancelled, superseded]);
|
||||
loading.release();
|
||||
releaseLoader = void 0;
|
||||
}
|
||||
if (!currentOperation() || !context || !sdk)
|
||||
return;
|
||||
failureNotice = "Error connecting to cast session";
|
||||
operationStarted = true;
|
||||
let current = context.getCurrentSession();
|
||||
if (!currentOperation())
|
||||
return;
|
||||
if (!current) {
|
||||
await Promise.race([context.requestSession(), cancelled, superseded]);
|
||||
if (!currentOperation())
|
||||
return;
|
||||
current = context.getCurrentSession();
|
||||
}
|
||||
if (!currentOperation())
|
||||
return;
|
||||
if (!current)
|
||||
throw new Error("No active Cast session");
|
||||
session = current;
|
||||
failureNotice = "Error casting media";
|
||||
await Promise.race([loadMedia(sdk, current, option, art.option.url), cancelled, superseded]);
|
||||
if (currentOperation()) {
|
||||
art.notice.show = "Casting started";
|
||||
option.onCastStart?.();
|
||||
}
|
||||
} catch (error) {
|
||||
if (currentOperation()) {
|
||||
art.notice.show = failureNotice;
|
||||
option.onError?.(error);
|
||||
throw error;
|
||||
}
|
||||
} finally {
|
||||
releaseLoader?.();
|
||||
releaseLoader = void 0;
|
||||
if (cancelOperation === stop)
|
||||
cancelOperation = void 0;
|
||||
operationStarted = false;
|
||||
}
|
||||
}
|
||||
const click = () => {
|
||||
if (!active())
|
||||
return Promise.resolve();
|
||||
if (!activeClick) {
|
||||
let resolve;
|
||||
let reject;
|
||||
activeClick = new Promise((yes, no) => {
|
||||
resolve = yes;
|
||||
reject = no;
|
||||
});
|
||||
activeClick.catch(() => {
|
||||
});
|
||||
cast().then(() => {
|
||||
activeClick = void 0;
|
||||
resolve();
|
||||
}, (error) => {
|
||||
activeClick = void 0;
|
||||
reject(error);
|
||||
});
|
||||
}
|
||||
return activeClick;
|
||||
};
|
||||
art.controls.add({
|
||||
name: "chromecast",
|
||||
position: "right",
|
||||
tooltip: "Chromecast",
|
||||
html: `<i class="art-icon art-icon-cast">${option.icon || icon}</i>`,
|
||||
mounted(control) {
|
||||
if (active())
|
||||
element = control;
|
||||
},
|
||||
click
|
||||
});
|
||||
art.on("destroy", destroy);
|
||||
if (art.isDestroy)
|
||||
destroy();
|
||||
return { name: "artplayerPluginChromecast", getCastState: () => state, isCasting: () => session !== null };
|
||||
}
|
||||
function artplayerPluginChromecast(option) {
|
||||
return async (art) => createController(art, option);
|
||||
}
|
||||
Object.defineProperty(artplayerPluginChromecast, "default", { value: artplayerPluginChromecast, writable: true, configurable: true });
|
||||
export {
|
||||
artplayerPluginChromecast as default
|
||||
};
|
||||
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Pro 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M512 96H64v99c-13-2-26.4-3-40-3H0V96C0 60.7 28.7 32 64 32H512c35.3 0 64 28.7 64 64V416c0 35.3-28.7 64-64 64H288V456c0-13.6-1-27-3-40H512V96zM24 224c128.1 0 232 103.9 232 232c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-101.6-82.4-184-184-184c-13.3 0-24-10.7-24-24s10.7-24 24-24zm8 192a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM0 344c0-13.3 10.7-24 24-24c75.1 0 136 60.9 136 136c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-48.6-39.4-88-88-88c-13.3 0-24-10.7-24-24z"/></svg>
|
||||
<svg height="20" width="20" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7V4h17v14h-8M3 11a10 10 0 0 1 10 10M3 16a5 5 0 0 1 5 5"/><circle cx="3" cy="21" r="1" fill="currentColor" stroke="none"/></svg>
|
||||
|
Before Width: | Height: | Size: 691 B After Width: | Height: | Size: 321 B |
@@ -0,0 +1,233 @@
|
||||
import type Artplayer from 'artplayer'
|
||||
import type { AvailabilityEvent, CastContext, CastSdk, CastSession, ChromecastOptions, ConnectionState, SessionEvent } from './types'
|
||||
import icon from './cast.svg?raw'
|
||||
import { loadMedia } from './media'
|
||||
import { configure, loadSdk } from './sdk'
|
||||
|
||||
export function createController(art: Artplayer, option: ChromecastOptions) {
|
||||
let disposed = false
|
||||
let sdk: CastSdk | undefined
|
||||
let context: CastContext | undefined
|
||||
let session: CastSession | null = null
|
||||
let state: string | null = null
|
||||
let activeClick: Promise<void> | undefined
|
||||
let releaseLoader: (() => void) | undefined
|
||||
let generation = 0
|
||||
let operationStarted = false
|
||||
let cancelOperation: (() => void) | undefined
|
||||
let cancel!: () => void
|
||||
const cancelled = new Promise<undefined>((resolve) => {
|
||||
cancel = () => resolve(undefined)
|
||||
})
|
||||
let element: HTMLElement | undefined
|
||||
const active = () => !disposed && !art.isDestroy
|
||||
const update = (value: ConnectionState) => {
|
||||
option.onStateChange?.(value)
|
||||
if (!active())
|
||||
return
|
||||
const button = element?.querySelector<HTMLElement>('.art-icon-cast')
|
||||
if (button)
|
||||
button.style.color = value === 'connected' ? 'red' : value === 'connecting' || value === 'disconnecting' ? 'orange' : 'white'
|
||||
}
|
||||
const onSession = (event: SessionEvent) => {
|
||||
if (!active() || !sdk)
|
||||
return
|
||||
state = event.sessionState
|
||||
const states = sdk.framework.SessionState
|
||||
if (state === states.NO_SESSION || state === states.SESSION_ENDED || state === states.SESSION_START_FAILED) {
|
||||
if (state !== states.NO_SESSION || operationStarted) {
|
||||
generation++
|
||||
cancelOperation?.()
|
||||
}
|
||||
session = null
|
||||
update('disconnected')
|
||||
}
|
||||
else {
|
||||
if (state === states.SESSION_ENDING || (session && event.session && event.session !== session)) {
|
||||
generation++
|
||||
cancelOperation?.()
|
||||
}
|
||||
session = event.session || null
|
||||
if (state === states.SESSION_STARTING)
|
||||
update('connecting')
|
||||
else if (state === states.SESSION_ENDING)
|
||||
update('disconnecting')
|
||||
else if (state === states.SESSION_STARTED || state === states.SESSION_RESUMED)
|
||||
update('connected')
|
||||
}
|
||||
}
|
||||
const onAvailability = (event: AvailabilityEvent) => {
|
||||
if (!active() || !sdk)
|
||||
return
|
||||
const states = sdk.framework.CastState
|
||||
if (event.castState === states.NO_DEVICES_AVAILABLE)
|
||||
option.onCastAvailable?.(false)
|
||||
else if (event.castState === states.NOT_CONNECTED || event.castState === states.CONNECTING || event.castState === states.CONNECTED)
|
||||
option.onCastAvailable?.(true)
|
||||
}
|
||||
const detach = () => {
|
||||
const ownedContext = context
|
||||
const ownedSdk = sdk
|
||||
context = undefined
|
||||
sdk = undefined
|
||||
if (ownedContext && ownedSdk) {
|
||||
const events = ownedSdk.framework.CastContextEventType
|
||||
const errors: unknown[] = []
|
||||
try {
|
||||
ownedContext.removeEventListener(events.SESSION_STATE_CHANGED, onSession)
|
||||
}
|
||||
catch (error) {
|
||||
errors.push(error)
|
||||
}
|
||||
try {
|
||||
ownedContext.removeEventListener(events.CAST_STATE_CHANGED, onAvailability)
|
||||
}
|
||||
catch (error) {
|
||||
errors.push(error)
|
||||
}
|
||||
if (errors.length)
|
||||
throw errors[0]
|
||||
}
|
||||
}
|
||||
const destroy = () => {
|
||||
if (disposed)
|
||||
return
|
||||
disposed = true
|
||||
cancel()
|
||||
const release = releaseLoader
|
||||
releaseLoader = undefined
|
||||
const errors: unknown[] = []
|
||||
for (const cleanup of [release, detach, () => art.off('destroy', destroy)]) {
|
||||
try {
|
||||
cleanup?.()
|
||||
}
|
||||
catch (error) {
|
||||
errors.push(error)
|
||||
}
|
||||
}
|
||||
session = null
|
||||
// ArtPlayer owns removal of its controls at player destruction.
|
||||
element = undefined
|
||||
if (errors.length)
|
||||
throw errors[0]
|
||||
}
|
||||
async function cast() {
|
||||
const epoch = generation
|
||||
const currentOperation = () => active() && generation === epoch
|
||||
let stop!: () => void
|
||||
const superseded = new Promise<undefined>((resolve) => {
|
||||
stop = () => resolve(undefined)
|
||||
})
|
||||
cancelOperation = stop
|
||||
let failureNotice = 'Failed to initialize Cast API'
|
||||
try {
|
||||
if (!sdk || !context) {
|
||||
const loading = loadSdk(option.sdk, (available) => {
|
||||
if (!currentOperation())
|
||||
return
|
||||
sdk = available
|
||||
context = configure(sdk)
|
||||
if (!currentOperation())
|
||||
return
|
||||
const events = sdk.framework.CastContextEventType
|
||||
try {
|
||||
context.addEventListener(events.SESSION_STATE_CHANGED, onSession)
|
||||
if (active())
|
||||
context.addEventListener(events.CAST_STATE_CHANGED, onAvailability)
|
||||
}
|
||||
catch (error) {
|
||||
try {
|
||||
detach()
|
||||
}
|
||||
catch {
|
||||
// Preserve the setup error; detach already tried both subscriptions.
|
||||
}
|
||||
throw error
|
||||
}
|
||||
})
|
||||
releaseLoader = loading.release
|
||||
await Promise.race([loading.promise, cancelled, superseded])
|
||||
loading.release()
|
||||
releaseLoader = undefined
|
||||
}
|
||||
if (!currentOperation() || !context || !sdk)
|
||||
return
|
||||
failureNotice = 'Error connecting to cast session'
|
||||
operationStarted = true
|
||||
let current = context.getCurrentSession()
|
||||
if (!currentOperation())
|
||||
return
|
||||
if (!current) {
|
||||
await Promise.race([context.requestSession(), cancelled, superseded])
|
||||
if (!currentOperation())
|
||||
return
|
||||
current = context.getCurrentSession()
|
||||
}
|
||||
if (!currentOperation())
|
||||
return
|
||||
if (!current)
|
||||
throw new Error('No active Cast session')
|
||||
session = current
|
||||
failureNotice = 'Error casting media'
|
||||
await Promise.race([loadMedia(sdk, current, option, art.option.url), cancelled, superseded])
|
||||
if (currentOperation()) {
|
||||
art.notice.show = 'Casting started'
|
||||
option.onCastStart?.()
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
if (currentOperation()) {
|
||||
art.notice.show = failureNotice
|
||||
option.onError?.(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
finally {
|
||||
releaseLoader?.()
|
||||
releaseLoader = undefined
|
||||
if (cancelOperation === stop)
|
||||
cancelOperation = undefined
|
||||
operationStarted = false
|
||||
}
|
||||
}
|
||||
const click = () => {
|
||||
if (!active())
|
||||
return Promise.resolve()
|
||||
if (!activeClick) {
|
||||
let resolve!: () => void
|
||||
let reject!: (error: unknown) => void
|
||||
activeClick = new Promise<void>((yes, no) => {
|
||||
resolve = yes
|
||||
reject = no
|
||||
})
|
||||
// ArtPlayer's DOM dispatcher ignores click results. Observe rejection there
|
||||
// while retaining the original rejecting Promise for programmatic callers.
|
||||
activeClick.catch(() => {})
|
||||
// Publish the guard before SDK setup can synchronously replay an event and
|
||||
// reenter click through a user's onStateChange callback.
|
||||
cast().then(() => {
|
||||
activeClick = undefined
|
||||
resolve()
|
||||
}, (error) => {
|
||||
activeClick = undefined
|
||||
reject(error)
|
||||
})
|
||||
}
|
||||
return activeClick
|
||||
}
|
||||
art.controls.add({
|
||||
name: 'chromecast',
|
||||
position: 'right',
|
||||
tooltip: 'Chromecast',
|
||||
html: `<i class="art-icon art-icon-cast">${option.icon || icon}</i>`,
|
||||
mounted(control) {
|
||||
if (active())
|
||||
element = control
|
||||
},
|
||||
click,
|
||||
})
|
||||
art.on('destroy', destroy)
|
||||
if (art.isDestroy)
|
||||
destroy()
|
||||
return { name: 'artplayerPluginChromecast', getCastState: () => state, isCasting: () => session !== null }
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
function loadScript(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script')
|
||||
script.src = src
|
||||
script.onload = resolve
|
||||
script.onerror = reject
|
||||
document.body.appendChild(script)
|
||||
})
|
||||
}
|
||||
|
||||
function getMimeType(url) {
|
||||
const extension = url.split('?')[0].split('#')[0].split('.').pop().toLowerCase()
|
||||
const mimeTypes = {
|
||||
mp4: 'video/mp4',
|
||||
webm: 'video/webm',
|
||||
ogg: 'video/ogg',
|
||||
ogv: 'video/ogg',
|
||||
mp3: 'audio/mp3',
|
||||
wav: 'audio/wav',
|
||||
flv: 'video/x-flv',
|
||||
mov: 'video/quicktime',
|
||||
avi: 'video/x-msvideo',
|
||||
wmv: 'video/x-ms-wmv',
|
||||
mpd: 'application/dash+xml',
|
||||
m3u8: 'application/x-mpegURL',
|
||||
}
|
||||
return mimeTypes[extension] || 'application/octet-stream'
|
||||
}
|
||||
|
||||
export default function artplayerPluginChromecast(option) {
|
||||
const DEFAULT_ICON = `<svg height="20" width="20" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M512 96H64v99c-13-2-26.4-3-40-3H0V96C0 60.7 28.7 32 64 32H512c35.3 0 64 28.7 64 64V416c0 35.3-28.7 64-64 64H288V456c0-13.6-1-27-3-40H512V96zM24 224c128.1 0 232 103.9 232 232c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-101.6-82.4-184-184-184c-13.3 0-24-10.7-24-24s10.7-24 24-24zm8 192a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM0 344c0-13.3 10.7-24 24-24c75.1 0 136 60.9 136 136c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-48.6-39.4-88-88-88c-13.3 0-24-10.7-24-24z"/></svg>`
|
||||
const DEFAULT_SDK = 'https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1'
|
||||
|
||||
let isCastInitialized = false
|
||||
let castSession = null
|
||||
let castState = null
|
||||
|
||||
const updateCastButton = (state) => {
|
||||
const button = document.querySelector('.art-icon-cast')
|
||||
if (button) {
|
||||
switch (state) {
|
||||
case 'connected':
|
||||
button.style.color = 'red'
|
||||
break
|
||||
case 'connecting':
|
||||
case 'disconnecting':
|
||||
button.style.color = 'orange'
|
||||
break
|
||||
case 'disconnected':
|
||||
default:
|
||||
button.style.color = 'white'
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const initializeCastApi = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
window.__onGCastApiAvailable = (isAvailable) => {
|
||||
if (isAvailable) {
|
||||
const context = window.cast.framework.CastContext.getInstance()
|
||||
context.setOptions({
|
||||
receiverApplicationId: window.chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID,
|
||||
autoJoinPolicy: window.chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED,
|
||||
})
|
||||
|
||||
// Listen for session state changes
|
||||
context.addEventListener(
|
||||
window.cast.framework.CastContextEventType.SESSION_STATE_CHANGED,
|
||||
(event) => {
|
||||
const SessionState = window.cast.framework.SessionState
|
||||
castState = event.sessionState
|
||||
castSession = event.session
|
||||
|
||||
switch (event.sessionState) {
|
||||
case SessionState.NO_SESSION:
|
||||
option.onStateChange?.('disconnected')
|
||||
updateCastButton('disconnected')
|
||||
break
|
||||
case SessionState.SESSION_STARTING:
|
||||
option.onStateChange?.('connecting')
|
||||
updateCastButton('connecting')
|
||||
break
|
||||
case SessionState.SESSION_STARTED:
|
||||
option.onStateChange?.('connected')
|
||||
updateCastButton('connected')
|
||||
break
|
||||
case SessionState.SESSION_ENDING:
|
||||
option.onStateChange?.('disconnecting')
|
||||
updateCastButton('disconnecting')
|
||||
break
|
||||
case SessionState.SESSION_RESUMED:
|
||||
option.onStateChange?.('connected')
|
||||
updateCastButton('connected')
|
||||
break
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// Listen for cast state changes
|
||||
context.addEventListener(window.cast.framework.CastContextEventType.CAST_STATE_CHANGED, (event) => {
|
||||
const CastState = window.cast.framework.CastState
|
||||
switch (event.castState) {
|
||||
case CastState.NO_DEVICES_AVAILABLE:
|
||||
option.onCastAvailable?.(false)
|
||||
break
|
||||
case CastState.NOT_CONNECTED:
|
||||
option.onCastAvailable?.(true)
|
||||
break
|
||||
case CastState.CONNECTING:
|
||||
case CastState.CONNECTED:
|
||||
option.onCastAvailable?.(true)
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
isCastInitialized = true
|
||||
resolve()
|
||||
}
|
||||
else {
|
||||
reject(new Error('Cast API is not available'))
|
||||
}
|
||||
}
|
||||
if (!window.chrome || !window.chrome.cast) {
|
||||
loadScript(option.sdk || DEFAULT_SDK).catch(reject)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const castVideo = (art, session) => {
|
||||
const url = option.url || art.option.url
|
||||
const mediaInfo = new window.chrome.cast.media.MediaInfo(url, option.mimeType || getMimeType(url))
|
||||
const request = new window.chrome.cast.media.LoadRequest(mediaInfo)
|
||||
session
|
||||
.loadMedia(request)
|
||||
.then(() => {
|
||||
art.notice.show = 'Casting started'
|
||||
option.onCastStart?.()
|
||||
})
|
||||
.catch((error) => {
|
||||
art.notice.show = 'Error casting media'
|
||||
option.onError?.(error)
|
||||
throw error
|
||||
})
|
||||
}
|
||||
|
||||
return async (art) => {
|
||||
art.controls.add({
|
||||
name: 'chromecast',
|
||||
position: 'right',
|
||||
tooltip: 'Chromecast',
|
||||
html: `<i class="art-icon art-icon-cast">${option.icon || DEFAULT_ICON}</i>`,
|
||||
click: async () => {
|
||||
if (!isCastInitialized) {
|
||||
try {
|
||||
await initializeCastApi()
|
||||
}
|
||||
catch (error) {
|
||||
art.notice.show = 'Failed to initialize Cast API'
|
||||
option.onError?.(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const context = window.cast.framework.CastContext.getInstance()
|
||||
if (castSession) {
|
||||
castVideo(art, castSession)
|
||||
}
|
||||
else {
|
||||
try {
|
||||
const session = await context.requestSession()
|
||||
castVideo(art, session)
|
||||
}
|
||||
catch (error) {
|
||||
art.notice.show = 'Error connecting to cast session'
|
||||
option.onError?.(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
name: 'artplayerPluginChromecast',
|
||||
getCastState: () => castState,
|
||||
isCasting: () => castSession !== null,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type Artplayer from 'artplayer'
|
||||
import type { ChromecastOptions } from './types'
|
||||
import { createController } from './controller'
|
||||
|
||||
export default function artplayerPluginChromecast(option: ChromecastOptions) {
|
||||
return async (art: Artplayer) => createController(art, option)
|
||||
}
|
||||
|
||||
// The 1.0 CommonJS namespace exposed .default; the current root exports a function.
|
||||
Object.defineProperty(artplayerPluginChromecast, 'default', { value: artplayerPluginChromecast, writable: true, configurable: true })
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { CastSdk, CastSession, ChromecastOptions } from './types'
|
||||
|
||||
const mimeTypes: Record<string, string> = {
|
||||
mp4: 'video/mp4',
|
||||
webm: 'video/webm',
|
||||
ogg: 'video/ogg',
|
||||
ogv: 'video/ogg',
|
||||
mp3: 'audio/mp3',
|
||||
wav: 'audio/wav',
|
||||
flv: 'video/x-flv',
|
||||
mov: 'video/quicktime',
|
||||
avi: 'video/x-msvideo',
|
||||
wmv: 'video/x-ms-wmv',
|
||||
mpd: 'application/dash+xml',
|
||||
m3u8: 'application/x-mpegURL',
|
||||
}
|
||||
|
||||
export function loadMedia(sdk: CastSdk, session: CastSession, option: ChromecastOptions, currentUrl: string): PromiseLike<unknown> {
|
||||
const url = option.url || currentUrl
|
||||
const extension = url.split('?')[0]!.split('#')[0]!.split('.').pop()!.toLowerCase()
|
||||
const info = new sdk.media.MediaInfo(url, option.mimeType || mimeTypes[extension] || 'application/octet-stream')
|
||||
return session.loadMedia(new sdk.media.LoadRequest(info))
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import type { CastContext, CastSdk, CastWindow } from './types'
|
||||
|
||||
const DEFAULT_SDK = 'https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1'
|
||||
interface Subscriber { ready: (sdk: CastSdk) => void, resolve: (sdk: CastSdk) => void, reject: (error: unknown) => void }
|
||||
interface Loading { subscribers: Set<Subscriber>, cancel: () => void }
|
||||
const pending = new WeakMap<CastWindow, Loading>()
|
||||
const configured = new WeakSet<CastContext>()
|
||||
|
||||
function ready(host: CastWindow): CastSdk | undefined {
|
||||
const framework = host.cast?.framework
|
||||
const api = host.chrome?.cast
|
||||
if (typeof framework?.CastContext?.getInstance === 'function'
|
||||
&& framework.CastContextEventType && framework.SessionState && framework.CastState
|
||||
&& typeof api?.media?.MediaInfo === 'function' && typeof api.media.LoadRequest === 'function'
|
||||
&& api.AutoJoinPolicy) { return { framework, media: api.media, autoJoinPolicy: api.AutoJoinPolicy.ORIGIN_SCOPED } }
|
||||
}
|
||||
|
||||
export function configure(sdk: CastSdk): CastContext {
|
||||
const context = sdk.framework.CastContext.getInstance()
|
||||
if (!configured.has(context)) {
|
||||
context.setOptions({ receiverApplicationId: sdk.media.DEFAULT_MEDIA_RECEIVER_APP_ID, autoJoinPolicy: sdk.autoJoinPolicy })
|
||||
configured.add(context)
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
function lease(loading: Loading, onReady: (sdk: CastSdk) => void) {
|
||||
let subscriber!: Subscriber
|
||||
const promise = new Promise<CastSdk>((resolve, reject) => {
|
||||
subscriber = { ready: onReady, resolve, reject }
|
||||
loading.subscribers.add(subscriber)
|
||||
})
|
||||
let released = false
|
||||
return { promise, release: () => {
|
||||
if (released)
|
||||
return
|
||||
released = true
|
||||
loading.subscribers.delete(subscriber)
|
||||
subscriber.reject(new Error('Cast initialization cancelled'))
|
||||
if (!loading.subscribers.size)
|
||||
loading.cancel()
|
||||
} }
|
||||
}
|
||||
|
||||
export function loadSdk(src: string | undefined, onReady: (sdk: CastSdk) => void) {
|
||||
const host = window as CastWindow
|
||||
const loaded = ready(host)
|
||||
if (loaded) {
|
||||
const promise = new Promise<CastSdk>((resolve, reject) => {
|
||||
try {
|
||||
onReady(loaded)
|
||||
resolve(loaded)
|
||||
}
|
||||
catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
return { promise, release: () => {} }
|
||||
}
|
||||
const existing = pending.get(host)
|
||||
if (existing)
|
||||
return lease(existing, onReady)
|
||||
|
||||
const loading: Loading = { subscribers: new Set(), cancel: () => finish(undefined, new Error('Cast initialization cancelled')) }
|
||||
pending.set(host, loading)
|
||||
const subscription = lease(loading, onReady)
|
||||
const previous = host.__onGCastApiAvailable
|
||||
let script: HTMLScriptElement | undefined
|
||||
let timer: number | undefined
|
||||
let settled = false
|
||||
function finish(sdk?: CastSdk, error?: unknown) {
|
||||
if (settled)
|
||||
return
|
||||
settled = true
|
||||
if (timer !== undefined)
|
||||
host.clearTimeout(timer)
|
||||
pending.delete(host)
|
||||
if (host.__onGCastApiAvailable === available)
|
||||
host.__onGCastApiAvailable = previous
|
||||
if (script) {
|
||||
script.onload = null
|
||||
script.onerror = null
|
||||
if (!sdk)
|
||||
script.remove()
|
||||
}
|
||||
for (const subscriber of loading.subscribers) {
|
||||
try {
|
||||
if (sdk) {
|
||||
subscriber.ready(sdk)
|
||||
subscriber.resolve(sdk)
|
||||
}
|
||||
else {
|
||||
subscriber.reject(error)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
subscriber.reject(error)
|
||||
}
|
||||
}
|
||||
loading.subscribers.clear()
|
||||
}
|
||||
function available(value: boolean, errorInfo?: string) {
|
||||
// The page's existing consumer keeps its callback and its exception semantics.
|
||||
// Always settle our own waiters even if that consumer throws.
|
||||
try {
|
||||
previous?.call(host, value, errorInfo)
|
||||
}
|
||||
finally {
|
||||
const sdk = value ? ready(host) : undefined
|
||||
finish(sdk, new Error('Cast API is not available'))
|
||||
}
|
||||
}
|
||||
host.__onGCastApiAvailable = available
|
||||
try {
|
||||
timer = host.setTimeout(() => finish(undefined, new Error('Timed out initializing Cast API')), 30000)
|
||||
script = document.createElement('script')
|
||||
script.src = src || DEFAULT_SDK
|
||||
script.onerror = error => finish(undefined, error)
|
||||
script.onload = () => {
|
||||
const sdk = ready(host)
|
||||
if (sdk)
|
||||
finish(sdk)
|
||||
}
|
||||
document.body.appendChild(script)
|
||||
}
|
||||
catch (error) {
|
||||
finish(undefined, error)
|
||||
}
|
||||
return subscription
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
export type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'disconnecting'
|
||||
|
||||
export interface ChromecastOptions {
|
||||
url?: string
|
||||
sdk?: string
|
||||
icon?: string
|
||||
mimeType?: string
|
||||
onStateChange?: (this: ChromecastOptions, state: ConnectionState) => void
|
||||
onCastAvailable?: (this: ChromecastOptions, available: boolean) => void
|
||||
onCastStart?: (this: ChromecastOptions) => void
|
||||
onError?: (this: ChromecastOptions, error: unknown) => void
|
||||
}
|
||||
|
||||
export interface MediaInfo {
|
||||
contentId: string
|
||||
contentType: string
|
||||
}
|
||||
|
||||
export interface LoadRequest { media: MediaInfo }
|
||||
export interface CastSession { loadMedia: (request: LoadRequest) => PromiseLike<unknown> }
|
||||
export interface SessionEvent { sessionState: string, session?: CastSession | null }
|
||||
export interface AvailabilityEvent { castState: string }
|
||||
|
||||
export interface CastContext {
|
||||
setOptions: (options: { receiverApplicationId: string, autoJoinPolicy: string }) => void
|
||||
getCurrentSession: () => CastSession | null
|
||||
requestSession: () => PromiseLike<unknown>
|
||||
addEventListener: (name: string, listener: (event: SessionEvent & AvailabilityEvent) => void) => void
|
||||
removeEventListener: (name: string, listener: (event: SessionEvent & AvailabilityEvent) => void) => void
|
||||
}
|
||||
|
||||
export interface CastSdk {
|
||||
framework: {
|
||||
CastContext: { getInstance: () => CastContext }
|
||||
CastContextEventType: { SESSION_STATE_CHANGED: string, CAST_STATE_CHANGED: string }
|
||||
SessionState: Record<string, string>
|
||||
CastState: Record<string, string>
|
||||
}
|
||||
media: {
|
||||
DEFAULT_MEDIA_RECEIVER_APP_ID: string
|
||||
MediaInfo: new (url: string, mimeType: string) => MediaInfo
|
||||
LoadRequest: new (media: MediaInfo) => LoadRequest
|
||||
}
|
||||
autoJoinPolicy: string
|
||||
}
|
||||
|
||||
export interface CastWindow extends Window {
|
||||
cast?: { framework?: CastSdk['framework'] }
|
||||
chrome?: { cast?: { media?: CastSdk['media'], AutoJoinPolicy?: { ORIGIN_SCOPED: string } } }
|
||||
__onGCastApiAvailable?: (available: boolean, errorInfo?: string) => void
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": { "allowJs": false },
|
||||
"include": ["src/**/*.ts", "../../types/assets.d.ts"]
|
||||
}
|
||||
Reference in new issue
Block a user