UNPKG

castable-video

Version:

Cast your video element to the big screen with ease!

209 lines (172 loc) 6.61 kB
/* global WeakRef */ export const privateProps = new WeakMap(); export class InvalidStateError extends Error {} export class NotSupportedError extends Error {} export class NotFoundError extends Error {} const HLS_RESPONSE_HEADERS = ['application/x-mpegURL','application/vnd.apple.mpegurl','audio/mpegurl'] // Fallback to a plain Set if WeakRef is not available. export const IterableWeakSet = globalThis.WeakRef ? class extends Set { add(el) { super.add(new WeakRef(el)); } forEach(fn) { super.forEach((ref) => { const value = ref.deref(); if (value) fn(value); }); } } : Set; export function onCastApiAvailable(callback) { if (!globalThis.chrome?.cast?.isAvailable) { globalThis.__onGCastApiAvailable = () => { // The globalThis.__onGCastApiAvailable callback alone is not reliable for // the added cast.framework. It's loaded in a separate JS file. // https://www.gstatic.com/eureka/clank/101/cast_sender.js // https://www.gstatic.com/cast/sdk/libs/sender/1.0/cast_framework.js customElements .whenDefined('google-cast-button') .then(callback); }; } else if (!globalThis.cast?.framework) { customElements .whenDefined('google-cast-button') .then(callback); } else { callback(); } } export function requiresCastFramework() { // todo: exclude for Android>=56 which supports the Remote Playback API natively. return globalThis.chrome; } export function loadCastFramework() { const sdkUrl = 'https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1'; if (globalThis.chrome?.cast || document.querySelector(`script[src="${sdkUrl}"]`)) return; const script = document.createElement('script'); script.src = sdkUrl; document.head.append(script); } export function castContext() { return globalThis.cast?.framework?.CastContext.getInstance(); } export function currentSession() { return castContext()?.getCurrentSession(); } export function currentMedia() { return currentSession()?.getSessionObj().media[0]; } export function editTracksInfo(request) { return new Promise((resolve, reject) => { currentMedia().editTracksInfo(request, resolve, reject); }); } export function getMediaStatus(request) { return new Promise((resolve, reject) => { currentMedia().getStatus(request, resolve, reject); }); } export function setCastOptions(options) { return castContext().setOptions({ ...getDefaultCastOptions(), ...options, }); } export function getDefaultCastOptions() { return { // Set the receiver application ID to your own (created in the // Google Cast Developer Console), or optionally // use the chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID receiverApplicationId: 'CC1AD845', // Auto join policy can be one of the following three: // ORIGIN_SCOPED - Auto connect from same appId and page origin // TAB_AND_ORIGIN_SCOPED - Auto connect from same appId, page origin, and tab // PAGE_SCOPED - No auto connect autoJoinPolicy: 'origin_scoped', // The following flag enables Cast Connect(requires Chrome 87 or higher) // https://developers.googleblog.com/2020/08/introducing-cast-connect-android-tv.html androidReceiverCompatible: false, language: 'en-US', resumeSavedSession: true, }; } //Get the segment format given the end of the URL (.m4s, .ts, etc) function getFormat(segment) { if (!segment) return undefined; const regex = /\.([a-zA-Z0-9]+)(?:\?.*)?$/; const match = segment.match(regex); return match ? match[1] : null; } function parseAudioRenditionUrl(playlistContent) { for (const line of playlistContent.split('\n')) { const trimmed = line.trim(); if (trimmed.startsWith('#EXT-X-MEDIA') && /TYPE=AUDIO/i.test(trimmed)) { const match = trimmed.match(/URI="([^"]+)"/i); if (match) return match[1]; } } return undefined; } function parsePlaylistUrls(playlistContent) { const lines = playlistContent.split('\n'); const urls = []; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); // Locate available video playlists and get the next line which is the URI (https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-17#section-4.4.6.2) if (line.startsWith('#EXT-X-STREAM-INF')) { const nextLine = lines[i + 1] ? lines[i + 1].trim() : ''; if (nextLine && !nextLine.startsWith('#')) { urls.push(nextLine); } } } return urls; } function parseSegment(playlistContent){ const lines = playlistContent.split('\n'); const url = lines.find(line => !line.trim().startsWith('#') && line.trim() !== ''); return url?.trim(); } export async function isHls(url) { if (!url) return false; if (/\.m3u8?(\?.*)?$/i.test(url)) return true; if (url.startsWith('blob:')) return false; try { const response = await fetch(url, {method: 'HEAD'}); const contentType = response.headers.get('Content-Type'); return HLS_RESPONSE_HEADERS.some((header) => contentType === header); } catch (err) { console.error('Error while trying to get the Content-Type of the manifest', err); return false; } } export async function getPlaylistSegmentFormat(url) { if (!url || url.startsWith('blob:')) return { videoFormat: undefined, audioFormat: undefined }; try { const mainManifestContent = await (await fetch(url)).text(); let videoChunksContent = mainManifestContent; const playlists = parsePlaylistUrls(mainManifestContent); if (playlists.length > 0) { const chosenPlaylistUrl = new URL(playlists[0], url).toString(); videoChunksContent = await (await fetch(chosenPlaylistUrl)).text(); } const videoSegment = parseSegment(videoChunksContent); const videoFormat = getFormat(videoSegment); const audioRenditionPath = parseAudioRenditionUrl(mainManifestContent); let audioFormat = videoFormat; if (audioRenditionPath) { try { const audioPlaylistUrl = new URL(audioRenditionPath, url).toString(); const audioChunksContent = await (await fetch(audioPlaylistUrl)).text(); const audioSegment = parseSegment(audioChunksContent); audioFormat = getFormat(audioSegment) ?? videoFormat; } catch (err) { console.error('Error while trying to parse the audio rendition playlist', err); } } return { videoFormat, audioFormat }; } catch (err) { console.error('Error while trying to parse the manifest playlist', err); return { videoFormat: undefined, audioFormat: undefined }; } }