Skip to content

去广告技术文档

1. 系统概述

本项目设计为M3U8播放地址处理工具,核心功能在于自动检测并移除嵌入在M3U8播放列表内的广告片段,同时兼容处理嵌套的M3U8链接。利用axios进行网络通信以检索M3U8文件,提供无广告的纯净播放体验。

去除网络插播广告虽满足部分用户需求,但需谨慎行事,考虑到可能引发的法律、技术及道德后果。用户在寻求改善在线体验的同时,应选择合法、安全的方式,并尊重内容创作者和平台的权益

2. 关键模块说明

2.1. Array.prototype.toReversed Polyfill

  • 目标: 确保在所有环境下均可使用数组的倒序功能。
  • 价值: 提高代码的跨环境兼容性。

2.2. URL解析与拼接 (resolve, urljoin)

  • 功能: 实现URL的正确解析与片段的拼接,特别是在处理M3U8内相对路径时。
  • 重要性: 确保资源引用的准确无误。

2.3. 广告检测与过滤核心逻辑 (fixAdM3u8Ai)

  • 职责:
    • 智能识别广告: 通过比对播放片段特征识别广告内容。
    • 嵌套处理: 处理M3U8列表中嵌套的其他M3U8链接。
    • URL修正: 转换所有播放片段URL为绝对路径。
  • 技术亮点:
    • 异步数据获取。
    • 精准的广告识别算法。
    • 详细的日志记录。
  • 工作流程:
      1. 输入: 接收用户提供的M3U8播放地址。
      1. 获取M3U8内容: 使用axios执行网络请求。
      1. 基础处理: 清理文本,转换URL为绝对形式。
      1. 广告识别与剔除:
      • 4.1. 分析首尾播放片段,计算相似度。
      • 4.2. 依据阈值移除疑似广告URL。
      1. 递归处理嵌套M3U8(如存在)。
      1. 输出: 返回处理后的纯净M3U8列表。
      1. 日志记录: 全程记录关键步骤和性能指标。

2.4. 遗留问题

  • 匹配精度
  • 同域名广告识别

3. 示例代码

代码由@hpindigo提供

点我查看示例代码
ts
import axios from 'axios';

if (typeof Array.prototype.toReversed !== 'function') {
  Object.defineProperty(Array.prototype, 'toReversed', {
    value: function () {
      const clonedList = this.slice();
      // 倒序新数组
      const reversedList = clonedList.reverse();
      return reversedList;
    },
    enumerable: false,
  });
}

const resolve = (from, to) => {
  const resolvedUrl = new URL(to, new URL(from, 'resolve://'));
  if (resolvedUrl.protocol === 'resolve:') {
    const { pathname, search, hash } = resolvedUrl;
    return pathname + search + hash;
  }
  return resolvedUrl.href;
};

/**
 *  url拼接
 * @param fromPath 初始当前页面url
 * @param nowPath 相对当前页面url
 * @returns {*}
 */
const urljoin = (fromPath, nowPath) => {
  fromPath = fromPath || '';
  nowPath = nowPath || '';
  return resolve(fromPath, nowPath);
};

/**
 *  智能对比去除广告。支持嵌套m3u8。只需要传入播放地址
 * @param m3u8_url m3u8播放地址
 * @param headers 自定义访问m3u8的请求头,可以不传
 * @returns {string}
 */
const fixAdM3u8Ai = async (m3u8_url: string, headers: object = {}) => {
  let ts = new Date().getTime();
  let option = headers;

  // 字符串比较
  function b(s1, s2) {
    let i = 0;
    while (i < s1.length) {
      if (s1[i] !== s2[i]) {
        break;
      }
      i++;
    }
    return i;
  }

  function reverseString(str) {
    return str.split('').reverse().join('');
  }

  let m3u8 = await axios({ url: m3u8_url, method: 'get', ...option });
  m3u8 = m3u8
    .trim()
    .split('\n')
    .map((it) => (it.startsWith('#') ? it : urljoin(m3u8_url, it)))
    .join('\n');
  m3u8 = m3u8.replace(/\n\n/gi, '\n');
  let last_url = m3u8.split('\n').slice(-1)[0];
  if (last_url.length < 5) {
    last_url = m3u8.split('\n').slice(-2)[0];
  }
  if (last_url.includes('.m3u8') && last_url !== m3u8_url) {
    m3u8_url = urljoin(m3u8_url, last_url);
    console.log('嵌套的m3u8_url:' + m3u8_url);
    m3u8 = await axios({ url: m3u8_url, method: 'get', ...option });
  }
  let s = m3u8
    .trim()
    .split('\n')
    .filter((it) => it.trim())
    .join('\n');
  let ss = s.split('\n');
  let firststr = '';
  let maxl = 0;
  let kk = 0;
  let kkk1 = 1;
  let kkk2 = 0;
  let secondstr = '';
  for (let i = 0; i < ss.length; i++) {
    let s = ss[i];
    if (!s.startsWith('#')) {
      if (kk == 0) firststr = s;
      if (kk > 0) {
        if (maxl > b(firststr, s) + 1) {
          if (secondstr.length < 5) secondstr = s;
          kkk2++;
        } else {
          maxl = b(firststr, s);
          kkk1++;
        }
      }
      kk++;
      if (kk >= 30) break;
    }
  }
  if (kkk2 > kkk1) firststr = secondstr;
  let firststrlen = firststr.length;
  let ml = Math.round(ss.length / 2).toString().length;
  let maxc = 0;
  let laststr = ss.toReversed().find((x) => {
    if (!x.startsWith('#')) {
      let k = b(reverseString(firststr), reverseString(x));
      maxl = b(firststr, x);
      maxc++;
      if (firststrlen - maxl <= ml + k || maxc > 10) {
        return true;
      }
    }
    return false;
  });
  console.log('最后一条切片:' + laststr);
  let ad_urls: string[] = [];
  for (let i = 0; i < ss.length; i++) {
    let s = ss[i];
    if (!s.startsWith('#')) {
      if (b(firststr, s) < maxl) {
        ad_urls.push(s);
        ss.splice(i - 1, 2);
        i = i - 2;
      } else {
        ss[i] = urljoin(m3u8_url, s);
      }
    } else {
      ss[i] = s.replace(/URI=\"(.*)\"/, 'URI="' + urljoin(m3u8_url, '$1') + '"');
    }
  }
  console.log('处理的m3u8地址:' + m3u8_url);
  console.log('----广告地址----');
  console.log(ad_urls);
  m3u8 = ss.join('\n');
  console.log('处理耗时:' + (new Date().getTime() - ts).toString());
  return m3u8;
};

export { fixAdM3u8Ai };

4. 结论

算法核心十分重要,微小的逻辑错误,可能引发连锁反应,导致误删非广告部分正常内容的关键数据。因此,在面临模棱两可的决策时,应更加谨慎细致,力求在保留数据的完整性与准确性的同时,实现体验的升级。

Released under the MIT License.