mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-21 19:53:43 +08:00
fix: player UX, ad fingerprinting, Android TV 9 white screen
Address issues #217 #218 and integrate the useful parts of PR #219 without regressions: stop player rebuild on episode reverse, grid/paged episodes, poster placeholders, chrome69 client transpile for Amlogic WebView, and duration-signature ad block detection while keeping interstitial/proxy filters. Bump to 4.9.15.
This commit is contained in:
@@ -211,15 +211,72 @@ export function learnMainPattern(blocks: Block[]): MainPattern {
|
||||
return { filenameRegex, avgDuration, commonPrefix, pathPrefix };
|
||||
}
|
||||
|
||||
/**
|
||||
* Find blocks that share an identical sequence of segment durations (fingerprint)
|
||||
* with another block in the playlist.
|
||||
*
|
||||
* If a block (with >= 3 segments) has an identical duration signature
|
||||
* as another block in the playlist, and it is not the main content block,
|
||||
* it is extremely likely to be a repeated inserted ad block.
|
||||
*/
|
||||
export function findDuplicateSignatureBlockIndices(blocks: Block[]): Set<number> {
|
||||
const duplicateIndices = new Set<number>();
|
||||
if (!blocks || blocks.length < 2) return duplicateIndices;
|
||||
|
||||
// Find the largest block (assumed main content block)
|
||||
let mainBlockIndex = -1;
|
||||
let maxSegments = 0;
|
||||
blocks.forEach((block, idx) => {
|
||||
if (block.segments.length > maxSegments) {
|
||||
maxSegments = block.segments.length;
|
||||
mainBlockIndex = idx;
|
||||
}
|
||||
});
|
||||
|
||||
// Map signature -> array of block indices
|
||||
const signatureMap = new Map<string, number[]>();
|
||||
|
||||
blocks.forEach((block, idx) => {
|
||||
// Require at least 3 segments to form a signature to prevent accidental single-segment collisions
|
||||
if (block.segments.length < 3) return;
|
||||
|
||||
// Signature based on segment durations rounded to 3 decimal places (milliseconds precision)
|
||||
const signature = block.segments.map(s => s.duration.toFixed(3)).join(',');
|
||||
|
||||
const existing = signatureMap.get(signature) || [];
|
||||
existing.push(idx);
|
||||
signatureMap.set(signature, existing);
|
||||
});
|
||||
|
||||
// Flag blocks whose signatures appear 2 or more times
|
||||
signatureMap.forEach((indices) => {
|
||||
if (indices.length >= 2) {
|
||||
indices.forEach(idx => {
|
||||
// Ensure we don't accidentally flag the main content block
|
||||
if (idx !== mainBlockIndex && blocks[idx].segments.length < maxSegments * 0.8) {
|
||||
duplicateIndices.add(idx);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return duplicateIndices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Score a block for ad likelihood based on heuristics
|
||||
* Returns a score where higher = more likely to be an ad
|
||||
*/
|
||||
export function scoreBlock(block: Block, mainPattern: MainPattern, extraKeywords: string[] = []): number {
|
||||
export function scoreBlock(
|
||||
block: Block,
|
||||
mainPattern: MainPattern,
|
||||
extraKeywords: string[] = [],
|
||||
isDuplicateSignature: boolean = false
|
||||
): number {
|
||||
let score = 0;
|
||||
|
||||
// If block has CUE tag, it's definitely an ad
|
||||
if (block.hasCueTag) {
|
||||
// If block has CUE tag or matches a duplicate signature, it's definitely an ad
|
||||
if (block.hasCueTag || isDuplicateSignature) {
|
||||
return 10; // Max score
|
||||
}
|
||||
|
||||
|
||||
+16
-6
@@ -3,7 +3,13 @@
|
||||
* Utility functions for M3U8 playlist manipulation
|
||||
*/
|
||||
|
||||
import { parseBlocks, learnMainPattern, scoreBlock, shouldFilterBlock } from './m3u8-ad-detector';
|
||||
import {
|
||||
parseBlocks,
|
||||
learnMainPattern,
|
||||
scoreBlock,
|
||||
shouldFilterBlock,
|
||||
findDuplicateSignatureBlockIndices,
|
||||
} from './m3u8-ad-detector';
|
||||
|
||||
const INTERSTITIAL_DATERANGE_MARKERS = [
|
||||
'class="com.apple.hls.interstitial"',
|
||||
@@ -67,7 +73,7 @@ function isAuxiliaryAdMetadataLine(trimmedLine: string, normalizedKeywords: stri
|
||||
* 1. Keyword matching (configurable via env)
|
||||
* 2. CUE-OUT/CUE-IN standard tags
|
||||
* 3. HLS interstitial metadata removal
|
||||
* 4. Heuristic block analysis (filename patterns, ad path keywords)
|
||||
* 4. Heuristic block analysis (filename patterns, ad path keywords, duration signature fingerprints)
|
||||
*
|
||||
* Also converts relative URLs to absolute URLs for Blob playback.
|
||||
*
|
||||
@@ -113,9 +119,13 @@ export function filterM3u8Ad(content: string, baseUrl: string, mode: AdFilterMod
|
||||
const blocks = parseBlocks(lines);
|
||||
if (blocks.length > 0) {
|
||||
const mainPattern = learnMainPattern(blocks);
|
||||
for (const block of blocks) {
|
||||
const duplicateIndices = findDuplicateSignatureBlockIndices(blocks);
|
||||
|
||||
for (let blockIdx = 0; blockIdx < blocks.length; blockIdx++) {
|
||||
const block = blocks[blockIdx];
|
||||
const isDuplicate = duplicateIndices.has(blockIdx);
|
||||
// Pass all keywords (including custom ones) to heuristic scorer
|
||||
const score = scoreBlock(block, mainPattern, normalizedKeywords);
|
||||
const score = scoreBlock(block, mainPattern, normalizedKeywords, isDuplicate);
|
||||
const threshold = mode === 'aggressive' ? 3.0 : 5.0;
|
||||
|
||||
if (shouldFilterBlock(score, threshold)) {
|
||||
@@ -125,8 +135,8 @@ export function filterM3u8Ad(content: string, baseUrl: string, mode: AdFilterMod
|
||||
adLineIndices.add(segment.lineIndex - 1); // EXTINF line
|
||||
}
|
||||
} else if (block.segments.length > 0) {
|
||||
// Segment-level detection:
|
||||
// Even if the whole block didn't trigger, check segments individually
|
||||
// Segment-level detection:
|
||||
// Even if the whole block didn't trigger, check segments individually
|
||||
// if it's a suspicious single-segment "block" (common for ads without discontinuity)
|
||||
for (const segment of block.segments) {
|
||||
const singleSegmentBlock = {
|
||||
|
||||
Reference in New Issue
Block a user