M3u8CacheRepository.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712
  1. <?php
  2. namespace App\Repositories\Utils;
  3. use App\Constants\VideoConstant;
  4. use App\Repositories\ConfigRepoistory;
  5. use App\Repositories\UserRepository;
  6. use Carbon\Carbon;
  7. use GuzzleHttp\Client;
  8. use Hyperf\HttpServer\Router\Router;
  9. use Psr\SimpleCache\CacheInterface;
  10. use function MongoDB\is_string_array;
  11. class M3u8CacheRepository
  12. {
  13. /**
  14. * @var null|CdnRepository
  15. */
  16. private static $cdnRepo = null;
  17. /**
  18. * @var null|Client
  19. */
  20. private static $httpClient = null;
  21. /**
  22. * 超期时间,20天.
  23. * @var float|int
  24. */
  25. private static $expiredSecond = 6000;
  26. private static $servers = [];
  27. /**
  28. * 签名key
  29. * @var string
  30. */
  31. private static $signKey = 'IeMqCoiSIkP71oRx';
  32. /**
  33. * 获取m3u8原始内容(不带鉴权)
  34. * @param string $url
  35. * @param bool $isFromCache 是否读取缓存.
  36. * @return array|false|mixed|string|string[]|null
  37. */
  38. public static function getM3u8OriginRaw($url, $isFromCache = true)
  39. {
  40. // 检查是否合法的url.
  41. if (!self::checkUrl($url)) {
  42. return '';
  43. }
  44. $n = stripos($url, '.m3u8');
  45. if ($n === false) {
  46. // 无效的扩展名.
  47. return '';
  48. }
  49. // 拿到没有query参数的url地址.
  50. $noQueryUrl = substr($url, 0, $n + strlen('.m3u8'));
  51. $strHash = md5($noQueryUrl);
  52. $fileName = $strHash . '.m3u8';
  53. $savePath = self::path();
  54. $cachePath = $savePath . '/'. self::childDir($strHash);
  55. $filePath = $cachePath . '/'. $fileName;
  56. if (!is_dir($cachePath)) {
  57. mkdir($cachePath, 0777, true);
  58. }
  59. // 设置超时时间.
  60. // set_time_limit(0);
  61. if (file_exists($filePath)) {
  62. $originExpiredSeconds = 120 * 24 * 60 * 60; // 原始文件保存120天.
  63. $modTime = filemtime($filePath);
  64. if ($modTime && abs(Carbon::now()->diffInSeconds(Carbon::parse($modTime))) > $originExpiredSeconds) {
  65. $data = self::saveToLocalByUrl($url, $filePath);
  66. } else {
  67. if ($isFromCache !== false) {
  68. $data = file_get_contents($filePath);
  69. } else {
  70. // 不走本地缓存也不保存.
  71. $data = self::getRemoteFileRaw($url);
  72. }
  73. }
  74. } else {
  75. $data = self::saveToLocalByUrl($url, $filePath);
  76. }
  77. return $data;
  78. }
  79. /**
  80. * 清理所有文件.
  81. * @return bool
  82. */
  83. public static function clearAll()
  84. {
  85. $m3u8Path = self::path();
  86. if (is_dir($m3u8Path)) {
  87. return rmdir($m3u8Path);
  88. }
  89. return false;
  90. }
  91. public static function path()
  92. {
  93. $path = base_path('storage/cache/m3u8');
  94. return $path;
  95. }
  96. public static function m3u8Urlpath()
  97. {
  98. $path = base_path('storage/cache/m3u8_url');
  99. return $path;
  100. }
  101. public static function childDir($strHash)
  102. {
  103. $dir1 = substr($strHash, 0, 4);
  104. $dir2 = substr($strHash, 4, 4);
  105. return $dir1 . '/' . $dir2;
  106. }
  107. public static function getCdnUrl($fieldUrl, $addAuth = true)
  108. {
  109. $querySign = '';
  110. if (strripos($fieldUrl, '|')) {
  111. list($flag, $uri) = explode('|', $fieldUrl);
  112. $servers = self::getAll();
  113. if (!isset($servers[$flag])) {
  114. return '';
  115. }
  116. $domain = $servers[$flag]['url'];
  117. $querySign = self::getCdnRepo()->getSignKeyValue($uri);
  118. $url = rtrim($domain, '/') . '/'. ltrim($uri, '/');
  119. } else {
  120. $arrUrl = parse_url($fieldUrl);
  121. if (!empty($arrUrl['path'])) {
  122. $uri = $arrUrl['path'];
  123. $querySign = self::getCdnRepo()->getSignKeyValue($uri);
  124. }
  125. $url = $fieldUrl;
  126. }
  127. $fullUrl = $url;
  128. if ($addAuth) {
  129. if (!empty($querySign)) {
  130. if (strripos($url, '?')) {
  131. $fullUrl .= '&'. $querySign;
  132. } else {
  133. $fullUrl .= '?' . $querySign;
  134. }
  135. }
  136. }
  137. return $fullUrl;
  138. }
  139. public static function getM3u8Url($fieldUrl, $addAuth = true)
  140. {
  141. $querySign = '';
  142. if (strripos($fieldUrl, '|')) {
  143. list($flag, $uri) = explode('|', $fieldUrl);
  144. $servers = self::getAll();
  145. if (!isset($servers[$flag])) {
  146. return '';
  147. }
  148. if (!empty($servers[$flag]['source_url'])) {
  149. $domain = $servers[$flag]['source_url'];
  150. } else {
  151. $domain = $servers[$flag]['url'];
  152. }
  153. // $domain = $servers[$flag]['url'];
  154. $querySign = self::getCdnRepo()->getSignKeyValue($uri);
  155. $url = rtrim($domain, '/') . '/'. ltrim($uri, '/');
  156. } else {
  157. $arrUrl = parse_url($fieldUrl);
  158. if (!empty($arrUrl['path'])) {
  159. $uri = $arrUrl['path'];
  160. $querySign = self::getCdnRepo()->getSignKeyValue($uri);
  161. }
  162. $url = $fieldUrl;
  163. }
  164. $fullUrl = $url;
  165. if ($addAuth) {
  166. if (!empty($querySign)) {
  167. if (strripos($url, '?')) {
  168. $fullUrl .= '&'. $querySign;
  169. } else {
  170. $fullUrl .= '?' . $querySign;
  171. }
  172. }
  173. }
  174. return $fullUrl;
  175. }
  176. /**
  177. * @param mixed $info
  178. * @param string $userCode
  179. * @param string $pkId
  180. * @param string $module
  181. * @return mixed|string
  182. * @throws \Psr\SimpleCache\InvalidArgumentException
  183. */
  184. public static function getVideoInnerUrl($info, $userCode = '', $pkId = '', $module = '')
  185. {
  186. if (is_array($info)) {
  187. $url = $info['url'];
  188. $isPreview = $info['is_preview'];
  189. $previewTime = isset($info['preview_time']) ? $info['preview_time'] : VideoConstant::PREVIEW_TIME;
  190. } else {
  191. $url = $info;
  192. $isPreview = false;
  193. $previewTime = VideoConstant::PREVIEW_TIME;
  194. }
  195. $userInfo = make(UserRepository::class)->getUserInfoFromCache($userCode);
  196. $authUrl = '';
  197. if (strripos($url, '.m3u8')) {
  198. $configRepo = make(ConfigRepoistory::class);
  199. // 路由基础前缀.
  200. $routerBasePrefix = '/app/api';
  201. // 取内容的url,从配置读取,解决swoole 无法获取端口问题.
  202. $safeProxyUrl = rtrim($configRepo->get('safe_proxy_url', 'sys', ''), '/');
  203. if (!empty($safeProxyUrl)) {
  204. $baseUrl = $safeProxyUrl;
  205. } else {
  206. $currenUrl = request()->url();
  207. $arrUrl = parse_url($currenUrl);
  208. $baseUrl = $arrUrl['scheme'] . '://' . $arrUrl['host'];
  209. if (!empty($arrUrl['port'])) {
  210. $baseUrl .= ':'. $arrUrl['port'];
  211. }
  212. }
  213. $previewFlag = 't5'; // 默认预览
  214. if ($isPreview !== true) {
  215. $previewFlag = 'k4'; // 标识 k4=完整,t5=预览
  216. }
  217. $originZuhe = $previewFlag . $userInfo['device_type'] . Carbon::now()->subDays(54)->format('md') . $userInfo['device_no'];
  218. $encodeZuhe = caesar_encode($originZuhe, 'l,n', 3); // 偏移了3
  219. $timestamp = Carbon::now()->timestamp;
  220. $sign = self::generateSign($originZuhe, $timestamp);
  221. if (!empty($pkId)) {
  222. $authUrl = $baseUrl . $routerBasePrefix . '/m3u8/raw';
  223. $queryParmas = [
  224. 'tk' => $encodeZuhe, // 偏移了6
  225. 'module' => $module,
  226. 'pk' => $pkId,
  227. 'sn' => $sign,
  228. 'tm' => $timestamp,
  229. // 'rate_type' => '', // 暂时注释,多分辨率
  230. ];
  231. $authUrl = $authUrl . '?' . http_build_query($queryParmas);
  232. } else {
  233. $authUrl = $baseUrl . $routerBasePrefix . '/m3u8/play/raw';
  234. $queryParmas = [
  235. 'tk' => $encodeZuhe, // 偏移了6
  236. 'module' => $module,
  237. 'sn' => $sign,
  238. 'tm' => $timestamp,
  239. // 'rate_type' => '', // 暂时注释,多分辨率
  240. ];
  241. $authUrl = $authUrl . '?' . http_build_query($queryParmas);
  242. }
  243. } else {
  244. $authUrl = $url;
  245. }
  246. return $authUrl;
  247. }
  248. /**
  249. * 获取视频url( 2小时有效,主要考虑query hash变动,影响缓存m3u8)
  250. * @param string $url
  251. * @param bool $isFromCache
  252. * @return mixed|string
  253. */
  254. public static function getVideoUrl($url, $isFromCache = true)
  255. {
  256. $authUrl = '';
  257. if (strripos($url, '.m3u8')) {
  258. $strHash = md5($url);
  259. $fileName = $strHash . '.url';
  260. $savePath = self::m3u8Urlpath();
  261. $cachePath = $savePath . '/'. self::childDir($strHash);
  262. $filePath = $cachePath . '/'. $fileName;
  263. if (!is_dir($cachePath)) {
  264. mkdir($cachePath, 0777, true);
  265. }
  266. // 设置超时时间.
  267. // set_time_limit(0);
  268. if (file_exists($filePath)) {
  269. $expiredSeconds = 2 * 60 * 60; // 原始文件保存2小时.
  270. $modTime = filemtime($filePath);
  271. if ($modTime && abs(Carbon::now()->diffInSeconds(Carbon::parse($modTime))) > $expiredSeconds) {
  272. $authUrl = M3u8CacheRepository::getCdnUrl($url, true);
  273. unlink($filePath);
  274. file_put_contents($filePath, $authUrl);
  275. } else {
  276. if ($isFromCache !== false) {
  277. $authUrl = file_get_contents($filePath);
  278. } else {
  279. // 不走本地缓存也不保存.
  280. $authUrl = M3u8CacheRepository::getCdnUrl($url, true);
  281. }
  282. }
  283. } else {
  284. $authUrl = M3u8CacheRepository::getCdnUrl($url, true);
  285. file_put_contents($filePath, $authUrl);
  286. }
  287. // $authUrl = M3u8CacheRepository::getCdnUrl($url, true);
  288. } else {
  289. $authUrl = $url;
  290. }
  291. return $authUrl;
  292. }
  293. /**
  294. * 获取图片url.
  295. * @param $url
  296. * @return mixed|string
  297. */
  298. public static function getImageUrl($url)
  299. {
  300. return self::getCdnUrl($url, true);
  301. }
  302. /**
  303. * 变量url
  304. * @param $url
  305. * @param array $params
  306. * @return array|mixed|string|string[]
  307. */
  308. public static function getLinkUrl($url, $params = [])
  309. {
  310. $originUrl = $url;
  311. $search = [];
  312. $replace = [];
  313. if (isset($params['user_code'])) {
  314. $search[] = '{user_code}';
  315. $replace[] = $params['user_code'];
  316. }
  317. if (!empty($search) && !empty($replace)) {
  318. $url = str_replace($search, $replace, $url);
  319. }
  320. return $url;
  321. }
  322. /**
  323. * 获取所有服务组信息(不缓存)
  324. * @return array
  325. */
  326. public static function getAllWithouCache()
  327. {
  328. $servers = [];
  329. $configRepo = make(ConfigRepoistory::class);
  330. $arrConfig = $configRepo->gets(['upload_url'], 'sys');
  331. $token = env('UPLOAD_TOKEN', 'GAkucH8');
  332. $url = rtrim($arrConfig['upload_url'], '/') . '/sg_server_config.php';
  333. $options = [
  334. 'query' => [
  335. 'upload_token' => $token,
  336. 'need_source_url' => 'yes_i_do', // 需要源url+port
  337. ],
  338. ];
  339. $raw = self::httpGet($url, $options);
  340. if (!empty($raw)) {
  341. $arrJson = json_decode($raw, true);
  342. if ($arrJson['code'] == 200) {
  343. $servers = $arrJson['data']['servers'];
  344. }
  345. }
  346. return $servers;
  347. }
  348. /**
  349. * 获取所有服务组信息(走缓存).
  350. * @return mixed
  351. */
  352. public static function getAll()
  353. {
  354. if (empty(self::$servers)) {
  355. $ttl = 3 * 60 * 60;
  356. $key = 'static_server';
  357. $cache = make(CacheInterface::class);
  358. self::$servers = $cache->remember($key, $ttl, function () {
  359. return self::getAllWithouCache();
  360. });
  361. }
  362. return self::$servers;
  363. }
  364. /**
  365. * 清除缓存.
  366. * @return bool
  367. * @throws \Psr\SimpleCache\InvalidArgumentException
  368. */
  369. public static function clearServersCache()
  370. {
  371. $key = 'static_server';
  372. $cache = make(CacheInterface::class);
  373. return $cache->delete($key);
  374. }
  375. /**
  376. * 获取m3u8的内容(带鉴权), 缓存query参数,优化目的浏览器本地缓存.
  377. * @param array $info
  378. * @param bool $isFromCache 是否从本地缓存.
  379. * @return string
  380. */
  381. public static function getMu38Raw($info, $isFromCache = true)
  382. {
  383. if (is_array($info)) {
  384. $url = $info['url'];
  385. $isPreview = $info['is_preview'];
  386. $previewTime = isset($info['preview_time']) ? $info['preview_time'] : VideoConstant::PREVIEW_TIME;
  387. $info['preview_time'] = $previewTime;
  388. } else {
  389. $url = $info;
  390. $isPreview = false;
  391. $previewTime = VideoConstant::PREVIEW_TIME;
  392. $info = [
  393. 'url' => $url,
  394. 'is_preview' => $isPreview,
  395. 'preview_time' => $previewTime,
  396. ];
  397. }
  398. $m3u8OriginUrl = self::getM3u8Url($url);
  399. $info2 = $info;
  400. $info2['origin_url'] = $m3u8OriginUrl;
  401. $fullUrl = self::getCdnUrl($url);
  402. $arrUrl = parse_url($fullUrl);
  403. $domain = $arrUrl['scheme'] . '://'. $arrUrl['host']; // eg:http://cn01.v.waawf.com
  404. $uriDir = dirname($arrUrl['path']); // eg:/20211130/Wc96M7Xr/1000kb/hls
  405. $uriPath = $arrUrl['path']; // eg:/20211130/Wc96M7Xr/1000kb/hls/index.m3u8
  406. if (!empty($arrUrl['port'])) {
  407. $domain .= ':'. $arrUrl['port'];// eg:http://cn01.v.waawf.com:8001
  408. }
  409. $urlPrefix = $domain . $uriDir; // eg:http://cn01.v.waawf.com/20211130/Wc96M7Xr/1000kb/hls
  410. $n = stripos($m3u8OriginUrl, '.m3u8');
  411. // 拿到没有query参数的url地址.
  412. $noQueryUrl = substr($m3u8OriginUrl, 0, $n + strlen('.m3u8'));
  413. $strHash = md5($noQueryUrl);
  414. $fileName = $strHash . '.cdn.m3u8';
  415. $hashPreviewTime = substr(md5($previewTime), 0, 6);
  416. $previewFileName = $strHash . '.cdn.preview.m3u8';
  417. $savePath = self::path();
  418. $cachePath = $savePath . '/'. self::childDir($strHash);
  419. $filePath = $cachePath . '/'. $fileName;
  420. $previewFilePath = $cachePath . '/'. $previewFileName;
  421. if (!is_dir($cachePath)) {
  422. mkdir($cachePath, 0777, true);
  423. }
  424. $getFilePath = $isPreview === true ? $previewFilePath : $filePath;
  425. // 设置超时时间.
  426. // set_time_limit(0);
  427. if (file_exists($filePath)) {
  428. $modTime = filemtime($filePath);
  429. if ($modTime && abs(Carbon::now()->diffInSeconds(Carbon::parse($modTime))) > self::$expiredSecond) {
  430. $data = self::saveToLocalByAuthRaw($info2, $filePath, $domain, $uriDir, $urlPrefix);
  431. } else {
  432. if ($isFromCache !== false) {
  433. $data = file_get_contents($getFilePath);
  434. } else {
  435. // 不走本地缓存也不保存.
  436. $data = self::getM3u8OriginRaw($m3u8OriginUrl);
  437. if ($isPreview) {
  438. $data = self::convertAuthPreviewRaw($data, $previewTime, $domain, $uriDir, $urlPrefix);
  439. } else {
  440. $data = self::convertAuthRaw($data, $domain, $uriDir, $urlPrefix);
  441. }
  442. }
  443. }
  444. } else {
  445. $data = self::saveToLocalByAuthRaw($info2, $filePath, $domain, $uriDir, $urlPrefix);
  446. }
  447. return $data;
  448. }
  449. /**
  450. * 生成签名
  451. * @param $originZu
  452. * @param $timestamp
  453. * @return string
  454. */
  455. public static function generateSign($originZu, $timestamp)
  456. {
  457. return md5($originZu . ':'. self::$signKey . ':'. strval($timestamp));
  458. }
  459. private static function checkUrl($url)
  460. {
  461. if (!preg_match("/^(http|https):\/\/[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+[\/=\?%\-&_~`@[\]\':+!]*([^<>\"\"])*$/", $url)) {
  462. return false;
  463. } else {
  464. return true;
  465. }
  466. }
  467. /**
  468. * 移除阿里云CDN自带的auth_key参数.
  469. * @param $data
  470. * @return array|mixed|string|string[]|null
  471. */
  472. private static function removeAliCdnOriginAuthQueryParams($data)
  473. {
  474. if (strripos($data, '?auth_key=')) {
  475. $pattern = '/(.+)\?auth_key=\d+\-[a-zA-Z0-9]+\-[a-zA-Z0-9]+\-[a-zA-Z0-9]+/';
  476. $data = preg_replace_callback($pattern, function ($matches) {
  477. return $matches[1];
  478. }, $data);
  479. }
  480. return $data;
  481. }
  482. private static function getRemoteFileRaw($url)
  483. {
  484. $options = [];
  485. $data = self::httpGet($url, $options);
  486. $data = self::removeAliCdnOriginAuthQueryParams($data);
  487. return $data;
  488. }
  489. private static function convertAuthRaw($content, $domain, $uriDir, $urlPrefix)
  490. {
  491. $pattern = "/\#EXTINF:([0-9\.]+),\n(.+)\.ts/i";
  492. $content = preg_replace_callback($pattern, function ($matches) use ($domain, $uriDir, $urlPrefix) {
  493. $relativeTsPath = $uriDir . '/' . $matches[2] . '.ts';
  494. // 不针对ts鉴权了.
  495. $querySign = self::getCdnRepo()->getSignKeyValue($relativeTsPath);
  496. $tsUrl = $domain . $relativeTsPath . '?'. $querySign;
  497. // $tsUrl = $domain . $relativeTsPath;
  498. $result = "#EXTINF:{$matches[1]},\n{$tsUrl}";
  499. return $result;
  500. }, $content);
  501. $pattern2 = "/\#EXT-X-KEY:METHOD=AES-128,URI=\"(.+)\"/i";
  502. $keyUrl = $urlPrefix . '/key.key';
  503. $replacement2 = "#EXT-X-KEY:METHOD=AES-128,URI=\"{$keyUrl}\"";
  504. $content = preg_replace([$pattern2],[$replacement2], $content, -1, $count);
  505. return $content;
  506. }
  507. /**
  508. * 转换预览视频内容(带鉴权)
  509. * @param $content
  510. * @param string $previewTime
  511. * @param $domain
  512. * @param $uriDir
  513. * @param $urlPrefix
  514. * @return array|string|string[]|null
  515. */
  516. private static function convertAuthPreviewRaw($content, $previewTime, $domain, $uriDir, $urlPrefix)
  517. {
  518. $pattern = "/\#EXTINF:([0-9\.]+),\n(.+)\.ts/i";
  519. $totalSeconds = 0;
  520. $startSeconds = 0;
  521. $endSeconds = 0;
  522. if (empty($previewTime) || $previewTime == '00:00:00-00:00:00') {
  523. // 没设置预览
  524. $endSeconds = 15;
  525. } else {
  526. $arrTimes = explode('-', $previewTime);
  527. if (count($arrTimes) != 2) {
  528. return '';
  529. }
  530. $startTime = $arrTimes[0];
  531. $endTime = $arrTimes[1];
  532. if (empty($startTime || empty($endTime))) {
  533. return '';
  534. }
  535. $baseStartObj = Carbon::parse('2022-11-11 00:00:00');
  536. $startObj = Carbon::parse('2022-11-11 '. $startTime);
  537. $endObj = Carbon::parse('2022-11-11 '. $endTime);
  538. if ($startObj >= $endObj) {
  539. return '';
  540. }
  541. $diffSeconds = $endObj->diffInSeconds($startObj);
  542. $startSeconds = abs($baseStartObj->diffInSeconds($startObj));
  543. $endSeconds = $startSeconds + $diffSeconds;
  544. }
  545. $content = preg_replace_callback($pattern, function ($matches) use ($domain, $uriDir, $urlPrefix, &$totalSeconds, $startSeconds, $endSeconds) {
  546. $result = '';
  547. $tsSecond = !empty($matches[1]) ? floatval($matches[1]) : 0;
  548. $totalSeconds = $totalSeconds + $tsSecond;
  549. if ($totalSeconds >= $startSeconds && $totalSeconds < $endSeconds) {
  550. $relativeTsPath = $uriDir . '/' . $matches[2] . '.ts';
  551. // 不针对ts鉴权了.
  552. $querySign = self::getCdnRepo()->getSignKeyValue($relativeTsPath);
  553. $tsUrl = $domain . $relativeTsPath . '?'. $querySign;
  554. // $tsUrl = $domain . $relativeTsPath;
  555. $result = "#EXTINF:{$matches[1]},\n{$tsUrl}";
  556. }
  557. return $result;
  558. }, $content);
  559. $content = preg_replace(['/[\n]{2,}/', '/\#EXT\-X\-ENDLIST/'], ['', "\n#EXT-X-ENDLIST"], $content); // 去掉多余的空行.
  560. $pattern2 = "/\#EXT-X-KEY:METHOD=AES-128,URI=\"(.+)\"/i";
  561. $keyUrl = $urlPrefix . '/key.key';
  562. $replacement2 = "#EXT-X-KEY:METHOD=AES-128,URI=\"{$keyUrl}\"\n";
  563. $content = preg_replace([$pattern2],[$replacement2], $content, -1, $count);
  564. return $content;
  565. }
  566. private static function getCdnRepo()
  567. {
  568. if (!self::$cdnRepo instanceof CdnRepository) {
  569. self::$cdnRepo = new CdnRepository();
  570. }
  571. return self::$cdnRepo;
  572. }
  573. /**
  574. * 保存至本地.
  575. * @param $url
  576. * @param $filePath
  577. * @return array|mixed|string|string[]|null
  578. */
  579. private static function saveToLocalByUrl($url, $filePath)
  580. {
  581. $data = self::getRemoteFileRaw($url);
  582. if (!empty($data)) {
  583. file_put_contents($filePath, $data);
  584. }
  585. return $data;
  586. }
  587. /**
  588. * @param array $info
  589. * @param $filePath
  590. * @param $domain
  591. * @param $uriDir
  592. * @param $urlPrefix
  593. * @return array|false|mixed|string|string[]|null
  594. */
  595. private static function saveToLocalByAuthRaw(array $info, $filePath, $domain, $uriDir, $urlPrefix)
  596. {
  597. $url = $info['origin_url'];
  598. $isPreview = $info['is_preview'];
  599. $previewTime = $info['preview_time'];
  600. $originContent = self::getM3u8OriginRaw($url, true);
  601. if (!empty($originContent)) {
  602. // 拿到没有query参数的url地址.
  603. $n = stripos($filePath, '.m3u8');
  604. $pathPrefix = substr($filePath, 0, $n);
  605. $hashPreviewTime = substr(md5($previewTime), 0, 6);
  606. $previewFilePath = $pathPrefix . '.preview.m3u8';
  607. $content = self::convertAuthRaw($originContent, $domain, $uriDir, $urlPrefix);
  608. file_put_contents($filePath, $content);
  609. $previewContent = self::convertAuthPreviewRaw($originContent, $previewTime, $domain, $uriDir, $urlPrefix);
  610. file_put_contents($previewFilePath, $previewContent);
  611. if ($isPreview) {
  612. return $previewContent;
  613. } else {
  614. return $content;
  615. }
  616. } else {
  617. return $originContent;
  618. }
  619. }
  620. /**
  621. * 获取http客户端.
  622. * @return Client|null
  623. */
  624. private static function getHttpClientRepo()
  625. {
  626. if (!self::$httpClient instanceof Client) {
  627. $options = array(
  628. 'timeout' => 20, // 超时,单位秒.
  629. 'verify' => false, // 是否校验SSL证书.
  630. 'http_errors' => false, // 如果跟随http异常头部是否中断.
  631. 'debug' => false, // 是否启用调试模式.
  632. 'allow_redirects' => false, // 不跟随301/302跳转.
  633. );
  634. self::$httpClient = new Client($options);
  635. }
  636. return self::$httpClient;
  637. }
  638. /**
  639. * GET获取.
  640. * @param $url
  641. * @param array $options
  642. * @return string
  643. * @throws \GuzzleHttp\Exception\GuzzleException
  644. */
  645. private static function httpGet($url, $options = [])
  646. {
  647. $raw = '';
  648. $ret = self::getHttpClientRepo()->get($url, $options);
  649. $httpStatus = $ret->getStatusCode();
  650. if ($httpStatus == 200) {
  651. $raw = $ret->getBody()->getContents();
  652. }
  653. return $raw;
  654. }
  655. }