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 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> 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 _speedController = BehaviorSubject(); // final PublishSubject _speedController = PublishSubject(); // final StreamController _speedController = StreamController.broadcast(); // 下载速度的流 //Stream get onVideoSpeedUpdate => _speedController.asBroadcastStream(); // 失败m3u8 列表 List failedM3u8List = []; /// localServer 闲时回调,应该返回一个remotePath AsyncValueGetter? onLocalServerIdel; HttpServer? _server; // 主缓存 BaseCacheManager _cacheManager; // 二级缓存 BaseCacheManager? _subCacheManager; bool _openSubManager = true; // 处理加密和解密的的函数 Map _customResponse = {}; /// 启动Competer,避免重复启动 Completer? _startCompleter; // 是否允许加入二级缓存 JoinSubCache? onJoinSubCache; ValueChanged? onErr; /// 请求拦截表 Map _reqFilterMap = {}; List 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 _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 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 localReqHeaders = {}; localReq.headers.forEach((String name, List 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 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(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>((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 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 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?> 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; 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? 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 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); }