网站首页 > java教程 正文
背景介绍
在视频点播系统中,广告视频片段常常嵌入到正常的视频播放流中,尤其是在 .m3u8 格式的播放列表文件中。传统的去广告方案需要识别广告的具体片段,这通常依赖于广告片段的已知标识。然而,在实际应用中,广告片段可能是通过插入不同的播放片段地址或者通过片段时长、顺序等特征来表现出来。因此,如何自动识别和去除广告,成为了一个挑战。
什么是 M3U8?
M3U8 是一个基于 UTF-8 编码的媒体播放列表格式,常用于流媒体视频播放。它包含了视频的播放顺序和每个视频片段的路径。M3U8 文件通常会引导播放器按顺序加载 .ts(MPEG-TS)格式的视频片段。
为什么需要去除广告?
在流媒体视频中,广告通常通过将广告视频片段插入到主视频流的中间进行播放。用户往往希望能够跳过或去除这些广告内容,以便享受无广告的观看体验。
需求分析
我们需要解决以下几个问题:
- 解析 .m3u8 文件,提取所有的 .ts 片段。
- 通过片段地址前缀的占比来识别正常的视频片段和广告片段。
- 去除广告片段,保留正常的视频片段。
- 生成新的 .m3u8 文件,并将其上传至 Minio 存储服务,供前端播放。
解决方案
我们通过以下步骤来实现这一方案:
- 解析 M3U8 文件:解析 .m3u8 文件,提取出所有的 .ts 片段。
2.统计片段地址前缀的出现次数:统计所有 .ts 片段的 前缀 部分(即去掉文件名后的路径部分)。计算出现频率,前缀出现次数最多的认为是 正常的视频片段地址。 - 通过前缀占比判断广告片段:如果某个 .ts 片段的前缀与正常前缀匹配,那么认为它是正常的视频片段。反之,认为它是广告片段并删除。
- 生成新的 M3U8 文件:去除广告片段,保留正常的视频片段,生成新的 M3U8 播放地址。
系统架构
- Spring Boot:提供后端服务接口,处理视频广告去除逻辑。
- MinIO:用于存储和管理 .ts 片段和 M3U8 文件。
主要步骤
配置 Spring Boot 和 MinIO
首先,我们需要在 Spring Boot 项目中集成 MinIO,用来存储视频文件(包括 .m3u8 文件和 .ts 片段)。以下是相关的配置:
# application.yml 配置文件
minio:
endpoint: minio.example.com
access-key: your-access-key
secret-key: your-secret-key
bucket: your-video-bucket
解析 M3U8 文件
M3U8 文件本质上是一个文本文件,包含了多个 .ts 文件的路径。在 Spring Boot 中,我们可以读取这个文件,解析出其中的片段信息。
import cn.hutool.core.util.URLUtil;
import lombok.extern.slf4j.Slf4j;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
/**
* @author klklmoon
*/
@Slf4j
public class M3U8ParserService {
public M3U8VO parse(String m3u8Url) {
M3U8VO m3u8 = new M3U8VO();
m3u8.setUrl(m3u8Url);
URI uri = URLUtil.toURI(m3u8Url);
String domain = uri.getScheme() + "://" + uri.getHost();
String path = getUrlPath(m3u8Url);
m3u8.setDomain(domain);
double duration = 0;
boolean discontinuity = false;
M3U8KeyVO m3u8Key = null;
M3U8StreamInfoVO streamInfo = null;
// 读取 M3U8 文件
try (InputStream inputStream = new URL(m3u8Url).openStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
if (line.startsWith("#EXTINF")) {
duration = parseExtInf(line);
} else if (line.startsWith("#EXT-X-VERSION")) {
parseExtXVersion(line, m3u8);
} else if (line.startsWith("#EXT-X-TARGETDURATION")) {
parseExtXTargetDuration(line, m3u8);
} else if (line.startsWith("#EXT-X-MEDIA-SEQUENCE")) {
parseExtXMediaSequence(line, m3u8);
} else if (line.startsWith("#EXT-X-PLAYLIST-TYPE")) {
parseExtXPlayListType(line, m3u8);
} else if (line.startsWith("#EXT-X-STREAM-INF")) {
streamInfo = parseExtXStreamInf(line);
} else if (streamInfo != null && !line.startsWith("#")) {
streamInfo.setUrl(buildSegmentUrl(line, domain, path));
m3u8.getStreams().add(streamInfo);
streamInfo = null;
} else if (line.startsWith("#EXT-X-KEY")) {
m3u8Key = parseExtXKey(line, m3u8, path);
if (!discontinuity) {
m3u8.setKey(parseExtXKey(line, m3u8,path));
}
} else if (line.startsWith("#EXT-X-DISCONTINUITY")) {
discontinuity = parseExtXDiscontinuity();
} else if (!line.startsWith("#")) {
M3U8SegmentVO segment = new M3U8SegmentVO();
segment.setUrl(buildSegmentUrl(line, domain, path));
segment.setDuration(duration);
segment.setDiscontinuity(discontinuity);
if (discontinuity && m3u8Key != null) {
segment.setKey(m3u8Key);
}
m3u8.getSegments().add(segment);
discontinuity = false;
}
}
} catch (Exception e) {
log.error("M3U8文件解析失败,m3u8文件地址:{},失败原因:{}", m3u8Url, e.getMessage());
}
return m3u8;
}
private double parseExtInf(String line) {
String durationString = line.split(":")[1].split(",")[0];
return Double.parseDouble(durationString);
}
private void parseExtXVersion(String line, M3U8VO m3u8) {
String version = line.split(":")[1];
m3u8.setVersion(version);
}
private void parseExtXTargetDuration(String line, M3U8VO m3u8) {
double targetDuration = Double.parseDouble(line.split(":")[1]);
m3u8.setTargetDuration(targetDuration);
}
private void parseExtXMediaSequence(String line, M3U8VO m3u8) {
int mediaSequence = Integer.parseInt(line.split(":")[1]);
m3u8.setMediaSequence(mediaSequence);
}
private void parseExtXPlayListType(String line, M3U8VO m3u8) {
String playListType = line.split(":")[1];
m3u8.setPlayListType(playListType);
}
private M3U8StreamInfoVO parseExtXStreamInf(String line) {
String[] parts = line.split(":")[1].split(",");
int bandwidth = 0;
String resolution = null;
for (String part : parts) {
if (part.startsWith("BANDWIDTH")) {
bandwidth = Integer.parseInt(part.split("=")[1]);
} else if (part.startsWith("RESOLUTION")) {
resolution = part.split("=")[1];
}
}
M3U8StreamInfoVO streamInfo = new M3U8StreamInfoVO();
streamInfo.setBandwidth(bandwidth);
streamInfo.setResolution(resolution);
return streamInfo;
}
private M3U8KeyVO parseExtXKey(String line, M3U8VO m3u8, String path) {
String[] parts = line.split(":")[1].split(",");
String method = null;
String uri = null;
String iv = null;
for (String part : parts) {
if (part.startsWith("METHOD")) {
method = part.split("=")[1];
} else if (part.startsWith("URI")) {
uri = part.split("=")[1].replace("\"", "");
} else if (part.startsWith("IV")) {
iv = part.split("=")[1];
}
}
M3U8KeyVO key = new M3U8KeyVO();
if (method != null) {
key.setMethod(method);
}
if (uri != null) {
key.setUri(buildSegmentUrl(uri, m3u8.getDomain(), path));
}
if (iv != null) {
key.setIv(iv);
}
return key;
}
private boolean parseExtXDiscontinuity() {
return true;
}
private String buildSegmentUrl(String line, String domain, String path) {
String url;
if (!line.contains("http") && !line.contains("https")) {
if (line.startsWith("/")) {
url = domain + line;
} else {
url = domain + path + line;
}
} else {
url = line;
}
return url;
}
private String getUrlPath(String url) {
URI uri = URLUtil.toURI(url);
String path = uri.getPath();
int lastSlashIndex = path.lastIndexOf('/');
// 如果找到了斜杠,则截取斜杠之前的部分
if (lastSlashIndex != -1) {
path = path.substring(0, lastSlashIndex + 1);
}
return path;
}
}
识别广告片段
假设广告片段可以通过文件名或某些特征来识别。例如,广告片段的 .ts 文件正常的视频.ts文件的路径不一致。我们可以遍历 .ts 文件列表,筛选出广告片段并标记。
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ArrayList;
public class AdRemover {
// 根据前缀占比来判断正常视频片段和广告片段
public List<String> removeAdsByPrefix(List<String> tsFiles) {
Map<String, Integer> prefixCountMap = new HashMap<>();
List<String> filteredFiles = new ArrayList<>();
// 统计每个前缀出现的次数
for (String tsFile : tsFiles) {
String prefix = getPrefix(tsFile);
prefixCountMap.put(prefix, prefixCountMap.getOrDefault(prefix, 0) + 1);
}
// 找出出现次数最多的前缀(认为是正常视频片段前缀)
String normalPrefix = getMostFrequentPrefix(prefixCountMap);
// 根据正常前缀,筛选出正常的片段
for (String tsFile : tsFiles) {
if (getPrefix(tsFile).equals(normalPrefix)) {
filteredFiles.add(tsFile);
}
}
return filteredFiles; // 返回去除广告后的片段列表
}
// 获取片段的前缀部分(即去掉文件名后的路径部分)
private String getPrefix(String tsFile) {
int lastSlashIndex = tsFile.lastIndexOf('/');
if (lastSlashIndex != -1) {
return tsFile.substring(0, lastSlashIndex + 1); // 获取路径部分
}
return tsFile; // 如果没有路径部分,则返回整个字符串
}
// 获取出现次数最多的前缀
private String getMostFrequentPrefix(Map<String, Integer> prefixCountMap) {
String mostFrequentPrefix = null;
int maxCount = 0;
for (Map.Entry<String, Integer> entry : prefixCountMap.entrySet()) {
if (entry.getValue() > maxCount) {
mostFrequentPrefix = entry.getKey();
maxCount = entry.getValue();
}
}
return mostFrequentPrefix;
}
}
生成新的 M3U8 文件
在去除广告片段后,我们需要生成新的 M3U8 文件,并返回一个新的播放地址。以下是生成新的 M3U8 文件的代码示例:
public String generateM3U8(M3U8VO m3u8) {
StringBuilder builder = new StringBuilder();
builder.append("#EXTM3U\n");
// 添加版本号
if (m3u8.getVersion() != null) {
builder.append("#EXT-X-VERSION:").append(m3u8.getVersion()).append("\n");
}
// 添加目标时长
builder.append("#EXT-X-TARGETDURATION:").append(m3u8.getTargetDuration()).append("\n");
// 添加媒体序列
builder.append("#EXT-X-MEDIA-SEQUENCE:").append(m3u8.getMediaSequence()).append("\n");
// 添加媒体序列
builder.append("#EXT-X-PLAYLIST-TYPE:").append(m3u8.getPlayListType()).append("\n");
// 添加加密信息
if (m3u8.getKey() != null) {
M3U8KeyVO key = m3u8.getKey();
builder.append("#EXT-X-KEY:METHOD=").append(key.getMethod());
if (key.getUri() != null) {
builder.append(",URI=\"").append(key.getUri()).append("\"");
}
if (key.getIv() != null) {
builder.append(",IV=").append(key.getIv());
}
builder.append("\n");
}
// 添加流信息
for (M3U8StreamInfoVO stream : m3u8.getStreams()) {
builder.append("#EXT-X-STREAM-INF:BANDWIDTH=").append(stream.getBandwidth());
if (stream.getResolution() != null) {
builder.append(",RESOLUTION=").append(stream.getResolution());
}
builder.append("\n").append(stream.getUrl()).append("\n");
}
// 添加片段信息
for (M3U8SegmentVO segment : m3u8.getSegments()) {
if (segment.isDiscontinuity()) {
builder.append("#EXT-X-DISCONTINUITY\n");
}
if (segment.isDiscontinuity() && segment.getKey() != null) {
builder.append("#EXT-X-KEY:METHOD=").append(segment.getKey().getMethod());
if (segment.getKey().getUri() != null) {
builder.append(",URI=\"").append(segment.getKey().getUri()).append("\"");
}
if (segment.getKey().getIv() != null) {
builder.append(",IV=").append(segment.getKey().getIv());
}
builder.append("\n");
}
builder.append("#EXTINF:").append(segment.getDuration()).append(",\n");
builder.append(segment.getUrl()).append("\n");
}
builder.append("#EXT-X-ENDLIST");
return builder.toString();
}
存储和提供 M3U8 文件
为了存储和提供生成的 M3U8 文件,我们可以将其上传到 MinIO,并返回一个可访问的 URL。
import io.minio.MinioClient;
import io.minio.errors.MinioException;
import io.minio.messages.Item;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.io.InputStream;
@Service
public class MinIOService {
@Value("${minio.bucket}")
private String bucketName;
private final MinioClient minioClient;
public MinIOService(MinioClient minioClient) {
this.minioClient = minioClient;
}
public String uploadFile(String filePath, InputStream fileStream) throws MinioException {
try {
minioClient.putObject(bucketName, filePath, fileStream);
return minioClient.getObjectUrl(bucketName, filePath);
} catch (Exception e) {
throw new MinioException("Error uploading file to MinIO", e);
}
}
}
完整流程
最终,Spring Boot 后端服务提供接口,结合上述各步骤的逻辑,完成广告删除的操作。以下是一个简化的流程:
- 接收请求:前端传入 M3U8 播放地址。
- 解析 M3U8 文件:读取并解析 M3U8 文件,提取出 .ts 文件列表。
- 去除广告片段:根据特征识别并删除广告片段。
- 生成新的 M3U8 文件:将剩余的 .ts 文件生成新的 M3U8 播放列表。
- 上传至 MinIO:将新的 M3U8 文件上传至 MinIO,并生成一个新的播放地址。
- 返回结果:返回新的播放地址给前端,用户可以播放去广告的视频。
示例代码:去广告接口
@RestController
@RequestMapping("/video")
public class VideoController {
@Autowired
private MinIOService minIOService;
@Autowired
private M3U8ParserService m3U8ParserService;
@Autowired
private AdRemover adRemover;
@Autowired
private M3U8Generator m3U8Generator;
@PostMapping("/remove-ads")
public CommonResult<String> removeAds(@RequestParam String m3u8Url) {
try {
// 步骤 1: 解析 m3u8 文件
List<String> tsFiles = m3U8ParserService.parse(m3u8Url);
// 步骤 2: 去除广告片段
List<String> filteredFiles = adRemover.removeAds(tsFiles);
// 步骤 3: 生成新的 m3u8 文件
String newM3U8File = "new_playlist.m3u8";
m3U8ParserService.generateM3U8(filteredFiles, newM3U8File);
// 步骤 4: 上传至 MinIO
String newPlayUrl = minIOService.uploadFile(newM3U8File, new FileInputStream(newM3U8File));
return CommonResult.success(newPlayUrl);
} catch (Exception e) {
return CommonResult.error(e.getMessage());
}
}
}
总结
通过使用 Spring Boot 和 MinIO,结合解析 M3U8 文件、去除广告片段和重新生成播放地址的逻辑,我们能够实现一个去除电影网站视频广告的功能。这种方法不仅提升了用户体验,还能有效地为视频流平台提供灵活的广告管理机制。
猜你喜欢
- 2025-01-21 有视频才有真相 两款录屏软件推荐
- 2025-01-21 第十三章:Java图形用户界面编程
- 2025-01-21 全新版Jetpack进阶提升,系统性落地短视频App|超清完结无秘
- 2025-01-21 如何实现在线视频播放?
- 2025-01-21 Java 中的屏幕共享
- 2025-01-21 用 Java 语言,写一个植物大战僵尸简易版
- 2025-01-21 JavaCV的摄像头实战之五:推流(我的直播梦)
- 2025-01-21 西瓜视频稳定性治理体系建设一:Tailor 原理及实践
- 2025-01-21 “易语言”基础知识——编写一个MP3播放器
- 2025-01-21 Java 监控直播流rtsp协议转rtmp、hls、httpflv协议返回浏览器
你 发表评论:
欢迎- 最近发表
-
- 多种负载均衡算法及其Java代码实现
- 输入www.baidu.com背后经历了啥?说清楚这个,已经超过90%的人了
- 优化MySQL:为什么你应该用 UNSIGNED INT 存储IP地址
- 实模式下CPU如何获取数据及指令(实模式寻址方式)
- java基础都在这了,小主们拿去吧(java基础是指什么)
- 盘点爬虫语言为何选择Python而不是Java
- 搭载Dubbo+Zookeeper踩了这么多坑,我终于决定写下这篇
- 网络协议之TCP/IP协议(面试必考内容) - javaEE初阶 - 细节狂魔
- 深夜报警!10亿次请求暴击,如何用Redis找出最热IP?
- VPN技术(IPsec/L2TP/SSLVPN/PPTP)学习笔记
- 标签列表
-
- java反编译工具 (77)
- java反射 (57)
- java接口 (61)
- java随机数 (63)
- java7下载 (59)
- java数据结构 (61)
- java 三目运算符 (65)
- java对象转map (63)
- Java继承 (69)
- java字符串替换 (60)
- 快速排序java (59)
- java并发编程 (58)
- java api文档 (60)
- centos安装java (57)
- java调用webservice接口 (61)
- java深拷贝 (61)
- 工厂模式java (59)
- java代理模式 (59)
- java.lang (57)
- java连接mysql数据库 (67)
- java重载 (68)
- java 循环语句 (66)
- java反序列化 (58)
- java时间函数 (60)
- java是值传递还是引用传递 (62)
本文暂时没有评论,来添加一个吧(●'◡'●)