简介:这是一套面向Android影视类应用开发者与个人站长的完整开源解决方案,涵盖后端采集系统、前端APP源码及全流程搭建教程,助力快速上线合规影视平台。资源共2000个文件,主体为603个Java核心业务逻辑文件、990个XML界面与配置文件、296个JSON接口定义与配置数据,辅以73个说明文本与12个Markdown文档,整体压缩包达563.54MB,结构清晰、模块解耦,便于二次开发与功能定制。已有501人学习下载,适用于具备Android基础与Java后端开发能力的中高级开发者。用户可直接部署运行,支持信天翁广告SDK集成、动态域名授权、五款主题切换、非会员6分钟限制、自动下一集、断点续看、热搜搜索及解析失败自动换源等实用功能;内容预览显示包含AvVideoController、VodService、IjkMediaPlayer等关键播放与服务模块,体现工程成熟度与实战适配性。
1. 绿豆影视6.0不是“拿来即用”的APK,而是一套可深度定制的影视类APP全栈源码体系
很多开发者第一次打开绿豆影视6.0压缩包时会愣住:里面没有编译好的APK,也没有一键部署脚本,反而塞满了.java文件、.docx教程和一堆带Controller、Service、Bean后缀的模块。这不是一个成品App,而是一套面向中小团队的影视聚合类应用快速落地框架——它把视频采集、接口解析、播放调度、广告集成、用户行为追踪等高频需求,拆解成可替换、可调试、可审计的Java+Spring Boot + Android原生组合。适合已有基础Android开发能力、能独立配置Nginx反向代理、熟悉MySQL建表与索引优化的工程师;不适合零基础想“改个图标就上线”的新手。它解决的核心问题是:如何在无第三方CMS依赖、不暴露授权逻辑、不强制绑定云服务的前提下,构建一套可控、可审计、可灰度发布的影视App闭环。本地授权机制和动态域名设计,让运营方真正掌握分发节奏与域名生命周期。
2. 后端影视采集系统:基于Spring Boot的插件化爬虫调度架构
绿豆影视6.0后端采用Spring Boot 2.7.x构建,核心采集逻辑封装在VodService.java与AvVideoController.java中,但关键设计在于其插件式采集引擎——所有站点解析规则不硬编码进主服务,而是以独立JAR包形式加载,通过@ComponentScan("com.lvdou.plugin.*")动态注入。这种设计规避了每次新增资源站都要重启服务的问题,也使规则更新具备热替换能力。
2.1 插件目录结构与加载机制
采集插件需按约定路径放置于/plugins/目录下,每个插件JAR包内必须包含:
META-INF/plugin.info:声明插件ID、版本、支持站点域名(如www.iqiyi.com)com.lvdou.plugin.xxx.XxxParser.class:实现VideoParser接口,重写parseDetail(String vid)与parseSearch(String keyword)方法application-plugin.yml:定义该插件专属HTTP超时、重试次数、User-Agent池
Spring Boot启动时通过PluginClassLoader扫描/plugins/,读取plugin.info并实例化对应Parser,存入ConcurrentHashMap<String, VideoParser>缓存。调用时根据请求URL域名匹配插件ID,避免正则全量匹配带来的性能损耗。
提示:
ScoreListBean.java与RecommendBean2.java并非业务实体,而是采集结果缓存中间对象,用于跨插件统一字段映射(如将优酷的playCount、腾讯的view_count统一转为score字段),减少前端解析适配成本。
2.2 关键配置项与参数调优表
| 配置项 | 默认值 | 说明 | 调优建议 |
|---|---|---|---|
lvdou.crawler.timeout-ms | 8000 | 单次HTTP请求超时 | 针对海外站点建议调至12000,避免误判失败 |
lvdou.crawler.retry-count | 2 | 解析失败重试次数 | 若目标站有反爬验证码,设为0并启用人工审核队列 |
lvdou.parser.cache-ttl | 3600 | 视频详情缓存有效期(秒) | 首页推荐位建议设为600,避免热门剧集数据过期 |
lvdou.ad.sdk-id | "" | 信天翁广告SDK AppID | 必须在application.yml中显式配置,否则广告位为空 |
以下为VodService.java中解析调度的核心逻辑片段:
// VodService.java 片段 public List<VodBean> search(String keyword) { List<VodBean> result = new ArrayList<>(); // 1. 并行调用所有已加载插件的search方法 CompletableFuture.allOf( pluginMap.values().stream() .map(parser -> CompletableFuture.supplyAsync(() -> parser.parseSearch(keyword), executor)) .toArray(CompletableFuture[]::new) ).join(); // 2. 合并结果并去重(按vid字段) pluginMap.values().forEach(parser -> { try { result.addAll(parser.parseSearch(keyword)); } catch (Exception e) { log.warn("Plugin {} search failed: {}", parser.getClass().getSimpleName(), e.getMessage()); } }); return result.stream() .collect(Collectors.collectingAndThen( Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(VodBean::getVid))), ArrayList::new )); }此代码体现两个关键设计:一是异步并行采集,避免单点故障拖慢整体响应;二是去重策略基于vid而非标题,防止同一剧集在不同源站重复入库。executor为自定义线程池,核心线程数=CPU核数×2,拒绝策略为CallerRunsPolicy,防止突发搜索请求压垮服务。
2.3 数据库表结构与索引优化要点
VodBean.java映射的主表vod_info包含47个字段,其中高频查询字段需针对性建索引:
-- 首页推荐位查询(按score倒序+type筛选) CREATE INDEX idx_vod_score_type ON vod_info(score DESC, type); -- 搜索关键词匹配(title字段需全文索引) ALTER TABLE vod_info ADD FULLTEXT(title, director, actor); -- 断点续看功能依赖的用户进度表 CREATE TABLE user_play_progress ( uid VARCHAR(32) NOT NULL, vid VARCHAR(64) NOT NULL, progress INT DEFAULT 0, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (uid, vid), INDEX idx_uid_updated (uid, updated_at) );注意:
VodService.java中存在两处同名文件(项目正文提及两次),实为历史版本残留。实际应保留src/main/java/com/lvdou/service/VodService.java,另一处位于/legacy/目录下,部署前需手动删除,否则Spring Boot会因Bean名称冲突启动失败。
3. 前端APP源码:Android原生架构下的播放稳定性与广告集成实践
绿豆影视6.0前端为纯Android Java项目(非Flutter或React Native),MainActivity.java作为入口Activity,通过Fragment管理首页、分类、搜索、个人中心四大模块。其技术选型聚焦播放稳定性与广告合规性,核心体现在IjkMediaPlayer.java封装与信天翁SDK集成方式上。
3.1 IjkPlayer深度定制:解决6.0版闪退根因
IjkMediaPlayer.java并非直接引用Bilibili开源库,而是基于ijkplayer-java0.8.8分支二次开发,重点修复两类崩溃:
- 内存泄漏:在
onDestroy()中显式调用mMediaPlayer.release()并置空引用,避免Activity销毁后SurfaceView仍持有Context; - 硬解异常:禁用
MediaCodec硬解模式(setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0)),强制使用ffmpeg软解,适配低端机型。
播放器初始化关键代码:
// MainActivity.java 片段 private void initPlayer() { IjkMediaPlayer.loadLibrariesOnce(null); // 预加载so库 IjkMediaPlayer.native_setLogLevel(IjkMediaPlayer.IJK_LOG_DEBUG); mMediaPlayer = new IjkMediaPlayer(); mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "opensles", 1); // 启用OpenSL ES音频输出 mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "overlay-format", 842225234); // RGBA mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 1); // 启动帧丢弃 // 关键修复:设置SurfaceView尺寸变更监听,避免横竖屏切换时Surface重建异常 surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() { @Override public void surfaceCreated(SurfaceHolder holder) { if (mMediaPlayer != null) { try { mMediaPlayer.setDisplay(holder.getSurface()); } catch (Exception e) { log.e("setDisplay failed", e); } } } // ... 其他回调省略 }); }参数说明:
opensles=1:启用OpenSL ES替代AudioTrack,降低音频延迟,提升多音轨切换流畅度;overlay-format=842225234:对应AV_PIX_FMT_RGBA,确保弹幕渲染层与视频层像素格式一致,避免绿屏;framedrop=1:开启帧丢弃,在网络抖动时主动丢弃B帧,保障播放连续性而非画面完整性。
3.2 信天翁SDK集成:广告展示与收益归因闭环
广告SDK集成在app/src/main/res/layout/activity_main.xml中预留AdContainerView,实际加载由AdManager.java控制:
// AdManager.java 片段 public void loadSplashAd(Activity activity, String adId) { // 1. 校验本地授权key是否匹配当前域名(动态域名机制) if (!DomainValidator.isValid(activity)) { return; // 拒绝加载广告,防止盗用 } // 2. 构造信天翁广告请求参数 Map<String, String> params = new HashMap<>(); params.put("app_id", BuildConfig.AD_APP_ID); // 从build.gradle注入 params.put("device_id", DeviceUtils.getDeviceId(activity)); // 取设备ID实现自动注册 params.put("ad_type", "splash"); // 开屏广告 // 3. 异步加载,超时10秒 new AdLoader(activity).load(params, new AdCallback() { @Override public void onAdLoaded(View adView) { adContainer.addView(adView); // 添加到预设容器 } @Override public void onAdFailed(String errorMsg) { log.w("Ad load failed: " + errorMsg); // 失败时降级为静态图片,不抛异常 adContainer.addView(new ImageView(activity)); } }); }提示:
BuildConfig.AD_APP_ID需在app/build.gradle中配置:android { buildTypes { release { buildConfigField "String", "AD_APP_ID", '"your_actual_ad_app_id"' } } }若未配置,SDK会返回
INVALID_APP_ID错误,且不会上报任何曝光数据,确保测试阶段无无效请求。
3.3 个人中心主题切换:资源动态加载与后台可控机制
五款主题通过ThemeManager.java管理,所有主题资源(背景图、颜色值、图标)均存于服务器,APP启动时拉取theme_config.json:
// theme_config.json 示例 { "active_theme": "theme_3", "themes": [ { "id": "theme_1", "bg_url": "https://cdn.example.com/theme1_bg.jpg", "primary_color": "#FF6B6B", "accent_color": "#4ECDC4" } ] }ThemeManager使用Glide异步加载背景图,并将颜色值存入SharedPreferences,所有UI组件通过ContextCompat.getColor()获取主题色。后台修改theme_config.json即可实时生效,无需发版。
4. 动态域名与本地授权:绕过云端验证的轻量级授权模型实现
绿豆影视6.0的授权机制摒弃传统“每次启动校验云端License”模式,采用打包时绑定+运行时校验的双阶段设计。核心逻辑在LicenseChecker.java中实现,其本质是将域名哈希值与授权Key进行AES-128-CBC加密比对,全程离线完成。
4.1 打包时域名注入与Key生成流程
开发者在打包APK前,需执行gradlew assembleRelease -Pdomain=yourdomain.com -PlicenseKey=your_secret_key。Gradle插件DomainInjector会:
- 将
yourdomain.com经SHA-256哈希后截取前16字节,作为AES加密IV; - 使用
your_secret_key(32字节)作为密钥,加密字符串"VALID_DOMAIN_" + domainHash; - 将密文写入
assets/license.dat,同时在BuildConfig.java中生成DOMAIN_HASH常量。
# 手动验证Key生成(供调试用) echo -n "yourdomain.com" | sha256sum | cut -c1-32 | xargs -I {} openssl enc -aes-128-cbc -K "your_secret_key_in_hex" -iv {} -in /dev/stdin -out license.dat4.2 运行时校验逻辑与失败降级
LicenseChecker.check()方法在Application.onCreate()中调用,流程如下:
public static boolean check(Context context) { try { String domain = getDomainFromWebView(context); // 从WebView加载的H5页面提取当前域名 String domainHash = hashDomain(domain); // 1. 读取assets/license.dat byte[] encrypted = readAsset(context, "license.dat"); // 2. AES解密(密钥来自BuildConfig.LICENSE_KEY) byte[] decrypted = AesUtil.decrypt(encrypted, BuildConfig.LICENSE_KEY.getBytes(StandardCharsets.UTF_8), domainHash.getBytes(StandardCharsets.UTF_8) ); // 3. 校验明文是否以"VALID_DOMAIN_"开头 return new String(decrypted, StandardCharsets.UTF_8).startsWith("VALID_DOMAIN_"); } catch (Exception e) { log.e("License check failed", e); return false; // 降级为无广告、无会员功能的免费版 } }注意:
getDomainFromWebView()通过注入JavaScript获取window.location.hostname,要求WebView已加载过业务页面。首次启动时若WebView未就绪,会触发onLicenseCheckFailed()回调,此时APP进入“演示模式”:首页显示水印、播放限制6分钟、禁用分享功能,但所有UI可正常操作,便于客户验收。
4.3 授权Key轮换与域名迁移实操步骤
当需要更换域名(如从old.com迁移到new.com)时,无需重新编译APK,只需三步:
- 后台更新
theme_config.json中的bg_url等资源链接指向新域名; - 用新域名重新生成License Key(保持
-PlicenseKey参数不变); - 将新生成的
license.dat覆盖APK assets目录,重签名后分发。
此过程验证了动态域名设计的真正价值:授权与内容分发解耦。运营人员可独立管理域名生命周期,技术侧仅需维护Key生成工具链。
5. 视频解析失败自动切换:JSON/普通接口双模解析容错机制
绿豆影视6.0支持两种解析模式——标准JSON接口(返回结构化数据)与普通HTML解析(正则提取播放地址),并在AvVideoController.java中实现失败自动降级。该机制不是简单重试,而是基于历史成功率建立动态权重路由。
5.1 解析器权重表与实时更新策略
系统维护一张内存权重表ParserWeightMap,初始值均为1.0:
| 解析器ID | 类型 | 当前权重 | 最近10次成功率 |
|---|---|---|---|
| json_api_v1 | JSON | 0.92 | 92% |
| html_parser_qq | HTML | 0.65 | 65% |
权重每小时更新一次,计算公式:new_weight = old_weight × 0.7 + (success_rate × 0.3)
成功率为过去100次调用中解析成功的比例(status=200且返回有效play_url)。
5.2 自动切换核心代码与参数说明
// AvVideoController.java 片段 public ResponseEntity<PlayUrlResponse> getPlayUrl(@RequestParam String vid) { List<ParserInfo> sortedParsers = parserWeightMap.getSortedParsers(); // 按权重降序 for (ParserInfo parser : sortedParsers) { try { PlayUrlResponse response = parser.invoke(vid); if (response.isValid()) { parserWeightMap.increaseWeight(parser.id); // 成功则权重+0.05 return ResponseEntity.ok(response); } } catch (Exception e) { parserWeightMap.decreaseWeight(parser.id); // 失败则权重-0.1 log.warn("Parser {} failed for vid {}: {}", parser.id, vid, e.getMessage()); } } // 全部失败时返回兜底方案 return ResponseEntity.status(503).body(PlayUrlResponse.empty()); }PlayUrlResponse.empty()返回预设的error.mp4占位视频,时长3秒,避免播放器黑屏卡死。该视频存于raw/error.mp4,由Gradle任务在打包时自动注入。
5.3 接口调试技巧:快速定位解析失败根源
当某视频无法播放时,按以下顺序排查:
- 确认vid有效性:用Postman访问
/api/vod/detail?vid=xxx,检查返回JSON中play_url字段是否为空; - 模拟解析器调用:在
AvVideoController.java中临时添加日志,打印parser.invoke(vid)的原始HTTP响应体; - 检查HTML解析正则:若为HTML解析器失败,打开目标网页源码,验证
Pattern.compile("href=\"(.*?)\".*?class=\"play-btn\"")是否匹配最新DOM结构; - 验证权重状态:在
/actuator/health端点查看parser-weight健康检查项,确认权重表是否被异常重置。
此机制使系统在单一资源站改版导致解析失效时,能在30秒内自动切至备用解析器,用户无感知。
本文还有配套的精品资源,点击获取