Files
huangguo_android/lib/hj_utils/local_server.dart
T
2026-09-15 15:44:13 +07:00

754 lines
26 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:hgdj/extension/extensions.dart';
import 'package:hgdj/hj_utils/text_util.dart';
import 'package:hgdj/hj_utils/video_cache_manager.dart';
import 'package:hgdj/tools_base/toast.dart';
import '../tools_base/cache/cancel_token_manager.dart';
import '../tools_base/net/load_apk/dio_cli.dart';
import 'file_util.dart';
const LOCAL_M3U8_FILTER = ".m3u8";
const LOCAL_TS_FILTER = ".ts";
const LOCAL_ALL_FILTER = ".*";
const LOCAL_SERVER_PING_PATH = "/__ping__";
// localserver把这个请求给外部处理的回调
typedef void CustomResponse(HttpRequest response);
const String local_server_tag = "cache-server";
/// 加密魔数头
const encryptMagicNumber = [0x88, 0xA8, 0x30, 0xCB, 0x10, 0x76];
/// 加密密钥
const ENCRYPT_KEY = 0xA3;
/// 是否是加密文件
bool _isEncryptData(List<int> buf) {
if (buf.empty() || buf.length < encryptMagicNumber.length) {
return false;
}
for (int iLoop = 0; iLoop < encryptMagicNumber.length; iLoop++) {
if (buf[iLoop] != encryptMagicNumber[iLoop]) return false;
}
return true;
}
/// 打印控制
csPrint(Object msg) {
// l.i(local_server_tag, "$msg", saveFile: false);
}
/// headers 的异步构造函数
typedef HeadersBuilder = Future<Map<String, String>> Function(Uri uri);
/// 是否加入二级缓存
typedef JoinSubCache = bool Function(String specialCharacters);
/// 单个请求拦截单元
class ReqFilter {
/// 要拦截的正则表达,这里是以.m3u8 和 .ts 结尾的请求
final String reg;
final String schema; //http, https
final String host; // www.qiNiu.com
final int? port; // null or 12345
final String pathPrefix; //m3u8 是api/app/vid/m3u8
final HeadersBuilder? headersBuilder;
ReqFilter(this.reg, this.schema, this.host,
{this.port, this.pathPrefix = "", this.headersBuilder})
: assert(TextUtil.isNotEmpty(reg)),
assert(TextUtil.isNotEmpty(host));
String toString() => Uri(scheme: schema, host: host, port: port).toString();
}
/// localServer 第三版 功能
/// 1,删除了以前依赖的flutter_cache_manager和文件锁,加快访问和减少维护成本;
/// 2,增加了网络请求重复的检测;
/// 3,增加了网络下载速度的功能;
/// note-this: 不要随意修改,修改之前先问下我
///
/// localServer 第四版 功能
/// 1,用cacheManager的请求接口盒请求去除重复来代替我们自己的;
/// 2,减少网络请求错误;
/// 3, 修复了一些bug;
/// 4,去掉了以前的下载速度;
/// 另外后期;1,可能把tasklist里面的cancel去掉;2,添加下载速度 okay
/// note-this: 不要随意修改,修改之前先问下我
///
/// localServer 第五版 功能
/// 实时返回响应数据
/// localServer 第六版 功能
/// 支持预缓存isPreCache
/// localServer 第七版 功能
/// 兼容色中色的localserver请求
/// localServer 第八版 功能
/// 支持任意文件任意请求异或加密,解密
class CacheServer {
static CacheServer? _instance;
int serverPort = 14587;
final _dio = createDio();
/// ts 流强制经过localserver
/// 一些ts流带了域名,会直接访问;不经过localserver缓存
bool forceThroghLocalServer = true;
/// 当前下载速度
int _nowVideoSpeed = 0;
/// 获取当前的下载速度
int get getVideoSpeed => _nowVideoSpeed;
// 下载速度控制器
//final BehaviorSubject<int> _speedController = BehaviorSubject<int>();
// final PublishSubject<int> _speedController = PublishSubject<int>();
// final StreamController<int> _speedController = StreamController.broadcast();
// 下载速度的流
//Stream<int> get onVideoSpeedUpdate => _speedController.asBroadcastStream();
// 失败m3u8 列表
List<String> failedM3u8List = [];
/// localServer 闲时回调,应该返回一个remotePath
AsyncValueGetter<String>? onLocalServerIdel;
HttpServer? _server;
// 主缓存
BaseCacheManager _cacheManager;
// 二级缓存
BaseCacheManager? _subCacheManager;
bool _openSubManager = true;
// 处理加密和解密的的函数
Map<String, CustomResponse> _customResponse = {};
/// 启动Competer,避免重复启动
Completer? _startCompleter;
// 是否允许加入二级缓存
JoinSubCache? onJoinSubCache;
ValueChanged<dynamic>? onErr;
/// 请求拦截表
Map<String, ReqFilter> _reqFilterMap = {};
List<String> cdnAddressLists = [];
String? selectLine;
factory CacheServer({
BaseCacheManager? cacheManager,
bool forceThroghLocalServer = true,
bool openSubManager = false,
}) {
if (_instance == null) {
cacheManager ??= VideoCacheManager();
_instance = CacheServer._(
cacheManager,
forceThroghLocalServer: forceThroghLocalServer,
openSubManager: openSubManager,
);
}
return _instance!;
}
void registerErrCallBack(ValueChanged onError) {
this.onErr = onError;
}
CacheServer._(
BaseCacheManager cacheManager, {
bool forceThroghLocalServer = true,
bool openSubManager = false,
}) : _cacheManager = cacheManager,
this.forceThroghLocalServer = forceThroghLocalServer,
this._openSubManager = openSubManager,
_subCacheManager = null;
/// 启动服务器
/// 有错返错,没错返null
Future start() async {
if (_startCompleter != null) {
csPrint("already has a startComplete");
return _startCompleter?.future;
}
_startCompleter = Completer();
csPrint("begin start localserver inner");
_startInner(); // async
return _startCompleter?.future;
}
/// 内部启动
/// [remoteCdnAndPathPrefix] http://cnd/app/vid/
_startInner() async {
// 先关闭原来的,再启动
await stop(true);
_server = await _bindAlways();
csPrint("_startInner()...begin listen:$serverPort");
_server?.listen(_onRequest,
onDone: _onServerDone, onError: _onServerError, cancelOnError: true);
csPrint("_bind success, start listen address:$localServerUri");
_startCompleter?.complete();
_startCompleter = null;
}
/// 一直启动绑定localServer只到成功
Future<HttpServer> _bindAlways() async {
while (true) {
try {
csPrint("_bindAlways()...serverPort:$serverPort");
return await HttpServer.bind(InternetAddress.loopbackIPv4, serverPort);
} catch (e) {
//l.e(local_server_tag, "bind fail $serverPort, err $e");
}
await Future.delayed(const Duration(milliseconds: 200));
serverPort++;
continue;
}
}
/// 服务器停止
/// [force] 强制停止,设置为true将会不等待请求,直接关闭链接
Future stop([bool force = false]) async {
if (_server != null) {
try {
csPrint("[LOCSERV] 关闭server");
await _server?.close(force: true);
} catch (e) {
//l.e(local_server_tag, "[LOCSERV] 关闭server失败,可能是因为服务被系统杀掉了");
}
}
}
/// 请求转发过滤器
/// 根据文件后缀转发
/// [reg] 正则表达式fileExtension需要带上. ".m3u8" ".ts"
void addReqFilter(String reg, String forwardUrl,
{bool force = false, String pathPrefix = "", HeadersBuilder? hb}) {
assert(reg.startsWith("."));
if (TextUtil.isEmpty(forwardUrl)) {
showToast("CDN地址为空");
}
final uri = Uri.tryParse(forwardUrl);
if (uri == null) {
//l.e(local_server_tag, "addReqFilter()...invalid url: $forwardUrl");
return;
}
// 转发结构体
final reqFilter = ReqFilter(reg, uri.scheme, uri.host,
port: uri.port, pathPrefix: pathPrefix, headersBuilder: hb);
if (force) {
// 不同的转发域,对应的转发函数
_reqFilterMap[reg] = reqFilter;
} else {
_reqFilterMap.putIfAbsent(reg, () => reqFilter);
}
}
/// 添加外部系统拦截调用,对于localserver一般是ttl
void addCustomResponse(String path, CustomResponse response,
{bool force = false}) {
if (force) {
_customResponse[path] = response;
} else {
_customResponse.putIfAbsent(path, () => response);
}
}
/// 获取localserver的信息
Uri get localServerUri =>
Uri(scheme: "http", host: _server?.address.host, port: _server?.port);
/// localserver的请求拦截
void _onRequest(HttpRequest request) {
switch (request.method) {
case "GET":
_handleGet(request);
break;
default:
// 时只支持GET
request.response.statusCode = HttpStatus.badRequest;
request.response.close();
}
}
void _onServerError(err) {
//l.e(local_server_tag, "_onServerError err $err");
}
void _onServerDone() {
csPrint("Server Closed.");
_server = null;
}
/// 拦截请求
Future _handleGet(HttpRequest localReq) async {
//这里处理其他层发送过来的ping
if (localReq.uri.path == LOCAL_SERVER_PING_PATH) {
localReq.response.statusCode = HttpStatus.ok;
localReq.response.close();
return;
}
csPrint("Receive ${localReq.method} request ${localReq.uri}");
// 处理ttl的加密
final response = _customResponse[localReq.uri.path];
if (response != null) {
csPrint("Custom Response ${localReq.uri}");
response.call(localReq);
return;
}
//存储文件的路径也是cache的Key
String cacheKey = getCacheKey(localReq.uri.path);
// FileInfo info = await _cacheManager.getFileFromCache(cachePath);
// //找到缓存,把文件中的数据当响应流返回
// if (null != info && info.file.existsSync()) {
// try {
// csPrint("Cache Hit path:$cachePath localUri:${localReq.uri}");
// await localReq.response.addStream(info.file.openRead());
// localReq.response.close();
// return;
// } on FileSystemException catch (e) {
// l.e(local_server_tag,
// "Cache Hit But FileSystemException ${localReq.uri} exception: $e");
// await _cacheManager.removeFile(cachePath);
// return;
// }
// }
//非文件的本地请求
final dot = localReq.uri.path.lastIndexOf(".");
if (dot < 0) {
csPrint("_handleGet path no registry ${localReq.uri}");
localReq.response.statusCode = HttpStatus.badRequest;
localReq.response.close();
return;
}
// 根据文件扩展名字获取请求过滤
final fileExtension = localReq.uri.path.substring(dot);
var reqFilter =
_reqFilterMap[fileExtension] ?? _reqFilterMap[LOCAL_ALL_FILTER];
if (reqFilter == null) {
//l.e(local_server_tag, "_handleGet path not find ${localReq.uri} reqFilter:$fileExtension");
localReq.response.statusCode = HttpStatus.badRequest;
localReq.response.close();
return;
}
// 是否是预缓存的请求
bool isPreCache = false;
Map<String, String> queryParameters = {};
queryParameters.addAll(localReq.uri.queryParameters ?? {});
if (queryParameters.containsKey("isPreCache") ?? false) {
queryParameters.remove("isPreCache");
isPreCache = true;
}
//构建远程请求和请求的headers
var remoteUri = localReq.uri.replace(
scheme: reqFilter.schema,
host: reqFilter.host,
port: reqFilter.port,
queryParameters:
fileExtension == LOCAL_TS_FILTER ? {} : queryParameters);
var remoteUriStr = remoteUri.toString();
if (remoteUriStr.endsWith("?")) {
remoteUri = Uri.parse(remoteUriStr.replaceAll("?", ""));
}
Map<String, String> localReqHeaders = {};
localReq.headers.forEach((String name, List<String> values) {
if (name == "host") return;
if (values.empty()) return;
// 暂时只取第一个
localReqHeaders[name] = values[0];
csPrint("request header:$name ${values[0]}");
});
if (reqFilter.headersBuilder != null) {
localReqHeaders.addAll(await reqFilter.headersBuilder!.call(remoteUri));
}
// localReqHeaders.remove("range");
csPrint("Cache Miss ${localReq.method} cachePath:$cacheKey => $remoteUri");
/// 获取远程文件,l里面调用了unawaited streamConroller close,不用担心Stream阻塞的问题
// await for (var fileResp in _cacheManager.getFileStream(remoteUri.toString(),
// headers: localReqHeaders)) {
// if (fileResp is FileInfo) {}
// }
Map<String, String> maps = {"CDN": selectLine ?? ""};
localReqHeaders.addAll(maps);
if (reqFilter.reg == LOCAL_ALL_FILTER
// &&localReqHeaders.containsKey(HttpHeaders.rangeHeader)
) {
csPrint("_getAllBytes()...直接请求开始:${remoteUri.toString()}");
var rangeStart = getRangeStart(localReqHeaders);
try {
// var oldToken = CancelTokenManager()
// .remove(remoteUri.toString(), rangeStart.toString());
// oldToken?.cancel('============>already have a same request :$rangeStart');
// var newToken = CancelTokenManager()
// .createToken(remoteUri.toString(), rangeStart.toString());
debugPrint("测试视频请求的header---- $localReqHeaders");
var resp = await _dio.get<ResponseBody>(remoteUri.toString(),
options: Options(
responseType: ResponseType.stream,
headers: localReqHeaders,
sendTimeout: const Duration(milliseconds: 85000),
receiveTimeout: const Duration(milliseconds: 85000),
),
cancelToken: null);
localReq.response.statusCode = resp.statusCode ?? HttpStatus.badRequest;
if (localReq.response.statusCode == HttpStatus.ok ||
localReq.response.statusCode == HttpStatus.accepted ||
localReq.response.statusCode == HttpStatus.created ||
localReq.response.statusCode == HttpStatus.partialContent) {
var s = resp.data?.stream;
bool isEncrypt = false;
var newS = s?.map<List<int>>((buf) {
if (rangeStart <= 0) {
isEncrypt = _isEncryptData(buf);
if (isEncrypt) {
return buf.sublist(encryptMagicNumber.length);
} else {
return buf;
}
} else {
return buf;
}
}).map((buf) {
return buf.map((it) => it ^ ENCRYPT_KEY).toList();
});
if (rangeStart <= 0) {
resp.data?.headers.forEach((key, values) {
if (HttpHeaders.contentLengthHeader == key) {
var ct = int.parse(values[0] ?? "0");
localReq.response.headers.add(key, ct > 0 ? (ct - 6) : ct);
csPrint("resp header ct:$key ${ct > 0 ? (ct - 6) : ct}");
} else {
csPrint("resp header:$key $values");
localReq.response.headers.add(key, values);
}
});
} else {
resp.data?.headers.forEach((key, values) {
csPrint("resp header:$key $values");
});
}
if (null != newS) {
await localReq.response.addStream(newS);
csPrint("===============>feed stream [success].......");
} else {
csPrint("===============>feed stream [failed].......");
_requestErr(localReq);
}
}
await localReq.response.close();
} catch (e) {
//l.e(local_server_tag, "getRangeFile()...error:$e");
//Logger().e("开始重新请求...........");
// _handleGet(localReq);
_requestErr(localReq);
} finally {
// CancelTokenManager()
// .remove(remoteUri.toString(), rangeStart.toString());
}
return;
}
var cacheManager = _getCacheManager(remoteUri.toString());
var s = cacheManager
.getFileStream(remoteUri.toString(),
headers: localReqHeaders,
withProgress: fileExtension != LOCAL_M3U8_FILTER)
.handleError(
(e) {
if (!isPreCache) {
this.onErr?.call(e);
}
// TODO 是否需要处理
//l.e(local_server_tag, "getFileStream()...开始处理远端请求$remoteUri 错误");
if (TextUtil.isNotEmpty(cacheKey) &&
cacheKey.contains(LOCAL_M3U8_FILTER) &&
!failedM3u8List.contains(cacheKey)) {
//l.e(local_server_tag, "getFileStream()...添加到失败错误列表$cacheKey");
failedM3u8List.add(cacheKey);
}
_requestErr(localReq);
},
// test: (error) {
// /// true 拦截任何错误
// l.e(local_server_tag, "getFileStream()...远端请求$remoteUri 发生错误:$error");
// _requestErr(localReq);
// return true;
// }
).where((fileResp) {
// 返回ture表示一直重试
if (fileResp is FileInfo) {
return true;
}
return true;
});
if (fileExtension.contains(LOCAL_M3U8_FILTER)) {
// 处理m3u8
await _handleM3u8(remoteUri, localReq, s, cacheManager);
} else if (fileExtension.contains(LOCAL_TS_FILTER)) {
// 处理ts
// await _handleTs(remoteUri, localReq, s, cacheManager, isPreCache);
} else {
// 处理所以文件
//await _handleAllFile(remoteUri, localReq, s, cacheManager, isPreCache);
}
}
/// 处理M3u8
_handleM3u8(Uri remoteUri, HttpRequest localReq, Stream<FileResponse> s,
BaseCacheManager cacheManager) async {
final sc = s.listen((f) {
if (null == f) {
//l.e(local_server_tag, "handleM3u8()...$remoteUri 来了个null鬼东西");
_requestErr(localReq);
return;
}
if (f is FileInfo) {
var len =
localReq.response.headers.value(HttpHeaders.contentLengthHeader);
if (null != len) {
// 避文件30天过期之后重新请求再次发送
//l.e(local_server_tag, "handleM3u8()...$remoteUri already send resp to user now skip");
return;
}
if (f.file.existsSync()) {
var source = f.file.readAsStringSync();
var sourceSize = source.length;
/// 强制ts请求通过local_server 主要是去除domin
if (forceThroghLocalServer) {
var lines = source.split("\n");
var tsLine = lines.firstWhere(
(it) => it.contains(LOCAL_TS_FILTER),
);
// csPrint("before source:$source");
if (TextUtil.isNotEmpty(tsLine) && tsLine.startsWith("http")) {
var domin = Uri.parse(tsLine ?? "").origin;
if (TextUtil.isNotEmpty(domin)) {
csPrint("need replace m3u8 inner host:$domin");
source = source.replaceAll(domin, "");
// csPrint("before source:$source");
}
}
}
var afterSize = source.length;
// localReq.response.headers
// .add(HttpHeaders.contentLengthHeader, afterSize);
// localReq.response.headers
// .add(HttpHeaders.contentTypeHeader, "application/octet-stream");
localReq.response.headers
.add(HttpHeaders.contentLengthHeader, afterSize);
localReq.response.headers
.add(HttpHeaders.contentTypeHeader, "application/octet-stream");
// localReq.response.contentLength = source.length;
localReq.response.add(source.codeUnits);
csPrint(
"handleM3u8()...本地请求$remoteUri 完成:sourceSize:$sourceSize afterSize:$afterSize");
localReq.response.close();
} else {
//l.e(local_server_tag, "handleM3u8()...本地请求$remoteUri 完成但是件不存在");
cacheManager.removeFile(f.originalUrl);
_requestErr(localReq);
}
} else {
// undo anything f is DownloadProgress
}
});
sc.onError((e) {
//l.e(local_server_tag, "handleM3u8()...发生了错误:$e");
_requestErr(localReq, sc: sc);
});
s.timeout(Duration(seconds: 7), onTimeout: (sink) {
//l.e(local_server_tag, "handleM3u8()...$remoteUri 超时了7秒没有反映,现在主动关闭请求");
_requestErr(localReq, sc: sc);
});
}
/// 请求错误
_requestErr(HttpRequest req, {StreamSubscription? sc}) async {
if (null != req) {
// if (null == req.response.statusCode) {
try {
req.response.statusCode = HttpStatus.badRequest;
} catch (e) {
//l.e(local_server_tag, "_requestErr()..." + req.response.toString());
}
// }
await req.response.close();
sc?.cancel();
}
}
BaseCacheManager _getCacheManager(String url) {
// var sc = FileUtil.getNamePrefix(url);
if (_openSubManager && (onJoinSubCache?.call(url) ?? false)) {
// csPrint("从用户缓存manager");
return _subCacheManager!;
} else {
// csPrint("从一般缓存manager");
return _cacheManager;
}
}
/// 取消m3u8和m3u8关联的有任
/// 这里取的是远程请求,不是m3u8 preload里面的task
void cancelM3u8(String localReqPath, [dynamic reason]) {
csPrint("cancelM3u8 $localReqPath");
if (TextUtil.isEmpty(localReqPath)) return;
var name = FileUtil.getName(localReqPath);
final prefix = FileUtil.getNamePrefix(localReqPath);
final suffix = FileUtil.getNameSuffix(localReqPath);
if (suffix != LOCAL_M3U8_FILTER) return;
//取消m3u8
final m3u8CancelToken = CancelTokenManager().remove(name);
if (null == m3u8CancelToken) return;
csPrint("cancelM3u8()...cancel m3u8的下载:$localReqPath");
m3u8CancelToken.cancel(reason);
//消ts
final List<String> removed = [];
CancelTokenManager().peekList.forEach((it) {
if (it.url.contains(prefix) && it.url.endsWith(LOCAL_TS_FILTER)) {
removed.add(it.url);
csPrint("cancelM3u8()...取消ts流的下载:$localReqPath");
it.token.cancel(reason);
}
});
for (final url in removed) {
CancelTokenManager().remove(url);
}
}
Future<Future<FileInfo?>?> getCacheFile(String remotePath) async {
// String remoteUrl = getRemoteUrl(remotePath);
if (TextUtil.isEmpty(remotePath)) return null;
// l.i(local_server_tag, "getCacheFile()...闲时缓存策略预测需要缓的url:$remoteUrl");
var cacheManager = _getCacheManager(remotePath);
var fileInfo = cacheManager.getFileFromMemory(getCacheKey(remotePath));
fileInfo ??= (await cacheManager.getFileFromCache(getCacheKey(remotePath)))
as Future<FileInfo?>;
return fileInfo;
}
String setSelectLine(String line) {
selectLine = line;
return selectLine!;
}
/// m3u8远程地址转本地地址 远程程地址格式特定要求如下:url必须以.m3u8结尾
/// [remotePath] 远程的播放路径 /xxx/xxx.m3u8
/// 返回localhost 127.0.0.1的地址
String? getLocalUrl(String remotePath, {Map<String, String>? queryParams}) {
// return "http://192.168.1.142:8080/video/hls/prog_index.m3u8";
// return "http://192.168.1.142:8080/video/hlss/index.m3u8";
// return "http://192.168.1.142:8080/video/hlss/index_abs.m3u8";
// return "http://192.168.1.142:8080/video/hls/prog_index_1.m3u8";
// return "http://202.60.250.122:9001/video/hls/prog_index_1.m3u8";
// return "https://fs.lhexm.com/sp/8a/67/lf/mg/605635d590d11f0679a058dbc013bd53.mp4";
// remotePath = "sp/r1/8p/mt/if/970dd3185c924ff29c273917615c844a.m3u8";
if (TextUtil.isEmpty(remotePath)) return null;
var remoteUri = Uri.parse(remotePath); //修正后的绝对
if (null == remoteUri) return null;
final dot = remoteUri.path.lastIndexOf(".");
if (dot <= 0 || dot >= remoteUri.path.length - 1) return null;
final fileExtension = remoteUri.path.substring(dot);
var reqFilter = _reqFilterMap[fileExtension];
// if (null == reqFilter) return null;
if (remotePath.startsWith("/")) {
// 兼容绝对路径 /sp/vid/xxxx.m3u8
remotePath = (reqFilter?.pathPrefix ?? "") + remotePath;
} else if (remotePath.startsWith("http")) {
// 兼容原始http和https
// undo https://xxx/sp/vid/xxxx.m3u8
} else {
// 兼容相对径 sp/vid/xxxx.m3u8
remotePath = (reqFilter?.pathPrefix ?? "") + "/" + remotePath;
}
remoteUri = Uri.parse(remotePath);
Map<String, String> query = {};
query.addAll(remoteUri.queryParameters);
query.addAll(queryParams ?? {});
// remoteUri.queryParameters.addAll(queryParams ?? {});
// final localUri = localServerUri;
if (!forceThroghLocalServer && remotePath.startsWith("http")) {
return remoteUri.replace(queryParameters: query).toString();
} else {
//这里替换local127.0.0.1
var localUrl = remoteUri
.replace(
scheme: localServerUri.scheme,
host: localServerUri.host,
port: localServerUri.port,
queryParameters: query)
.toString();
return localUrl;
}
}
/// 获取远程路径
/// [remotePath] 远程的播放路径 /xxx/xxx.m3u8,或者本地请求
String? getRemoteUrl(String remotePath) {
if (TextUtil.isEmpty(remotePath)) return null;
final dot = remotePath.lastIndexOf(".");
if (dot <= 0 || dot >= remotePath.length - 1) return null;
final fileExtension = remotePath.substring(dot);
var reqFilter = _reqFilterMap[fileExtension];
if (null == reqFilter) return null;
if (remotePath.startsWith("/")) {
// 兼容绝对路径 sp/vid/xxxx.m3u8
remotePath = reqFilter.pathPrefix + remotePath;
} else if (remotePath.startsWith("http")) {
// undo https://xxx/sp/vid/xxxx.m3u8
} else {
// 兼容相对路径 sp/vid/xxxx.m3u8
remotePath = reqFilter.pathPrefix + "/" + remotePath;
}
var uri = Uri.parse(remotePath); //修正后的绝对路径
if (null == uri) return null;
//这里替换远程
final remoteUri = uri.replace(
scheme: reqFilter.schema, host: reqFilter.host, port: reqFilter.port);
return remoteUri.toString();
}
}
/// url/绝对路径和相对路径
String getCacheKey(String url) {
assert(null != url);
return FileUtil.getName(url);
}