mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-23 05:41: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
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user