Files
pole-book/server/node_modules/castable-video/castable-utils.js

178 lines
5.3 KiB
JavaScript

/* 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 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;
}
export async function isHls(url) {
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) {
try {
const mainManifestContent = await (await fetch(url)).text();
let availableChunksContent = mainManifestContent;
const playlists = parsePlaylistUrls(mainManifestContent);
if (playlists.length > 0) {
const chosenPlaylistUrl = new URL(playlists[0], url).toString();
availableChunksContent = await (await fetch(chosenPlaylistUrl)).text();
}
const segment = parseSegment(availableChunksContent);
const format = getFormat(segment);
return format
} catch (err) {
console.error('Error while trying to parse the manifest playlist', err);
return undefined;
}
}