feat: full HLS.js support — detection via interceptors + deep probe, playback via hls.js in viewer #2
No files matched your search
@@ -13,9 +13,6 @@
|
||||
"test:watch": "vitest",
|
||||
"check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"hls.js": "^1.5.17"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ajv": "^8.17.1",
|
||||
"ajv-formats": "^3.0.1",
|
||||
|
||||
@@ -18,13 +18,9 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
switch (message.type) {
|
||||
case 'PING':
|
||||
sendResponse({ pong: true, timestamp: Date.now() });
|
||||
break;
|
||||
return;
|
||||
|
||||
default:
|
||||
console.warn('[goon-plugin] Unknown message type:', message.type);
|
||||
sendResponse({ error: `Unknown type: ${message.type}` });
|
||||
break;
|
||||
}
|
||||
|
||||
return true; // keep sendResponse channel open for async work
|
||||
});
|
||||
+2
-2
@@ -14,10 +14,10 @@
|
||||
|
||||
/**
|
||||
* Media delivery format of the video source.
|
||||
* - 'direct' : plain MP4/WebM blob served via HTTP(S)
|
||||
* - 'video' : plain MP4/WebM blob served via HTTP(S)
|
||||
* - 'hls' : HTTP Live Streaming manifest (.m3u8)
|
||||
*/
|
||||
export type MediaType = 'direct' | 'hls';
|
||||
export type MediaType = 'video' | 'hls';
|
||||
|
||||
/**
|
||||
* A single video entry discovered by a tab scan.
|
||||
|
||||
+31
-20
@@ -171,8 +171,23 @@ function m3u8DeepProbeFn() {
|
||||
// NOT a reverse-lookup. It returns its input unchanged — Hls.cast(video) just
|
||||
// returns the <video> element with no .url property. Real players store the
|
||||
// HLS instance directly on the video element as a custom property.
|
||||
// Only check non-standard properties (skip built-in <video> attributes).
|
||||
const STANDARD_VIDEO_ATTRS = new Set([
|
||||
'src', 'srcObject', 'crossOrigin', 'preload', 'autoplay', 'muted',
|
||||
'defaultMuted', 'loop', 'controls', 'controlsList', 'disableRemotePlayback',
|
||||
'volume', 'playbackRate', 'defaultPlaybackRate', 'playbackState',
|
||||
'currentTime', 'startTime', 'duration', 'buffered', 'seekable',
|
||||
'played', 'seeking', 'ended', 'readyState', 'networkState',
|
||||
'videoWidth', 'videoHeight', 'error', 'textTracks', 'videoTracks',
|
||||
'audioTracks', 'loadingBeganAt', 'label', 'id', 'className',
|
||||
'classList', 'style', 'attributes', 'dataset', 'innerHTML', 'textContent',
|
||||
'outerHTML', 'outerText', 'slot', 'accessKey', 'draggable', 'hidden',
|
||||
'lang', 'spellcheck', 'tabIndex', 'title', 'translate',
|
||||
'width', 'height', 'poster', 'preload', 'referrerPolicy',
|
||||
]);
|
||||
for (const v of document.querySelectorAll('video')) {
|
||||
for (const key of Object.keys(v)) {
|
||||
if (STANDARD_VIDEO_ATTRS.has(key)) continue;
|
||||
const obj = v[key];
|
||||
if (!obj || typeof obj !== 'object') continue;
|
||||
// Check for common HLS instance patterns on video elements
|
||||
@@ -193,23 +208,6 @@ function m3u8DeepProbeFn() {
|
||||
}
|
||||
}
|
||||
|
||||
// B3: Fallback — walk video element keys looking for HLS-related objects
|
||||
for (const v of document.querySelectorAll('video')) {
|
||||
for (const key of Object.keys(v)) {
|
||||
if (key.toLowerCase().includes('hls') || key === 'player') {
|
||||
const hlsObj = v[key];
|
||||
if (hlsObj && typeof hlsObj === 'object') {
|
||||
for (const subKey of Object.keys(hlsObj)) {
|
||||
if (typeof hlsObj[subKey] === 'string' && hlsObj[subKey].includes('.m3u8')) {
|
||||
if (!seen.has(hlsObj[subKey])) { seen.add(hlsObj[subKey]); manifests.push({ url: hlsObj[subKey], player: `video.${key}.${subKey}` }); }
|
||||
}
|
||||
}
|
||||
walk(hlsObj, `video.${key}`, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy C: Inline scripts
|
||||
for (const script of document.querySelectorAll('script:not([src])')) {
|
||||
const text = script.textContent || '';
|
||||
@@ -281,11 +279,24 @@ function scrapeFn(hlsManifests) {
|
||||
mediaType = 'hls';
|
||||
src = hlsManifest;
|
||||
}
|
||||
// Priority 3: Scan video element properties for HLS instance references.
|
||||
// Hls.cast() is a TypeScript identity cast in hls.js — NOT a reverse-lookup.
|
||||
// Real players store the HLS instance on the video element as custom properties.
|
||||
// Priority 3: Scan video element custom properties for HLS instance references.
|
||||
// Only check non-standard (custom) properties — skip built-in <video> attrs.
|
||||
if (!hlsManifest) {
|
||||
const stdAttrs = new Set([
|
||||
'src', 'srcObject', 'crossOrigin', 'preload', 'autoplay', 'muted',
|
||||
'defaultMuted', 'loop', 'controls', 'controlsList', 'disableRemotePlayback',
|
||||
'volume', 'playbackRate', 'defaultPlaybackRate', 'playbackState',
|
||||
'currentTime', 'startTime', 'duration', 'buffered', 'seekable',
|
||||
'played', 'seeking', 'ended', 'readyState', 'networkState',
|
||||
'videoWidth', 'videoHeight', 'error', 'textTracks', 'videoTracks',
|
||||
'audioTracks', 'loadingBeganAt', 'label', 'id', 'className',
|
||||
'classList', 'style', 'attributes', 'dataset', 'innerHTML', 'textContent',
|
||||
'outerHTML', 'outerText', 'slot', 'accessKey', 'draggable', 'hidden',
|
||||
'lang', 'spellcheck', 'tabIndex', 'title', 'translate',
|
||||
'width', 'height', 'poster', 'referrerPolicy',
|
||||
]);
|
||||
for (const key of Object.keys(v)) {
|
||||
if (stdAttrs.has(key)) continue;
|
||||
const obj = v[key];
|
||||
if (obj && typeof obj === 'object' && typeof obj.url === 'string') {
|
||||
const u = obj.url.toLowerCase();
|
||||
|
||||
@@ -170,7 +170,9 @@ const PHASE_ICONS = {
|
||||
loading: '\u2B07', // down arrow
|
||||
buffering: '\u27F7', // spinning
|
||||
playing: '\u25B6', // play
|
||||
switching: '\u21BB', // right arrow corner
|
||||
error: '\u2717', // X
|
||||
ended: '\u23F9', // stop
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -413,7 +415,6 @@ function wireHlsEvents(hls, video, panel) {
|
||||
state.lastActivity = Date.now();
|
||||
state.bufferedAhead = 0;
|
||||
state.segmentsLoaded = 0;
|
||||
state.segmentsBuffered = 0;
|
||||
renderPanelState(video, panel);
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user