BioTorrents.de’s version of Gazelle
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

mediainfo.class.php 23KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808
  1. <?php
  2. class MediaInfo
  3. {
  4. public static function parse($string, $raw=false)
  5. {
  6. $t = new ParseManager($string);
  7. $t->parse();
  8. return $raw ? $t->output_raw() : $t->output();
  9. }
  10. }
  11. class ParseManager
  12. {
  13. protected $lines;
  14. protected $parsed_lines;
  15. protected $index;
  16. protected $parsers;
  17. protected $output;
  18. protected $available_parsers = array(
  19. 'general'=> 'GeneralSectionParser',
  20. 'video'=> 'VideoSectionParser',
  21. 'audio'=> 'AudioSectionParser',
  22. 'text'=> 'TextSectionParser',
  23. );
  24. const GENERIC_PARSER = 'generic_parser';
  25. const MEDIAINFO_START = 'general';
  26. public function __construct($string='')
  27. {
  28. $this->index = 0;
  29. $this->output = '';
  30. $this->parsed_lines = [];
  31. $this->set_string($string);
  32. $p = new SectionParser();
  33. $this->add_parser($p);
  34. }
  35. protected function set_string($string)
  36. {
  37. $string = trim($string);
  38. $string = static::strip_escaped_tags($string);
  39. $lines = preg_split("/\r\n|\n|\r/", $string);
  40. array_walk($lines, function (&$l) {
  41. $l=trim($l);
  42. });
  43. $this->lines = $lines;
  44. }
  45. protected function add_parser($p, $name='')
  46. {
  47. $p->set_lines($this->lines, $this->index);
  48. if (!$name) {
  49. $name = static::GENERIC_PARSER;
  50. }
  51. $this->parsers[$name][] = $p;
  52. }
  53. public function parse()
  54. {
  55. // Get the next section
  56. while ($this->index < count($this->lines) &&
  57. !($line = $this->parsers[static::GENERIC_PARSER][0]->parse_line()));
  58. $section = SectionParser::section_name($line);
  59. $this->index--; // go back to line we just read
  60. // We can have multiple mediainfo files inside the block, so handle that case here
  61. if ($section == self::MEDIAINFO_START && isset($this->parsers[$section])) {
  62. $this->new_mediainfo();
  63. }
  64. if (isset($this->available_parsers[$section])) {
  65. $parser = new $this->available_parsers[$section];
  66. $this->add_parser($parser, $section);
  67. // Parse section using the parser
  68. while ($line = $parser->parse_line()) {
  69. $this->parsed_lines[] = $line;
  70. }
  71. $this->parsed_lines[] = '';
  72. } else {
  73. // Skip until the next blank line or until the next general section
  74. while ($line = $this->parsers[static::GENERIC_PARSER][0]->parse_line()) {
  75. $section = SectionParser::section_name($line);
  76. if ($section == self::MEDIAINFO_START) {
  77. $this->index--; // go back to line we just read
  78. break;
  79. }
  80. }
  81. }
  82. // Keep iterating until the last line
  83. if ($this->index < count($this->lines)) {
  84. $this->parse();
  85. }
  86. }
  87. public function output($cummulative=true)
  88. {
  89. $string = implode("<br />\n", $this->parsed_lines);
  90. if (!isset($this->parsers['general'])) {
  91. return $string;
  92. }
  93. $midiv_start = '<div class="spoilerContainer hideContainer">
  94. <input type="button" class="spoilerButton" value="Show '.
  95. $this->parsers['general'][0]->filename.
  96. '" /><blockquote class="spoiler hidden">';
  97. $midiv_end = "</blockquote></div>";
  98. $output = '<table class="mediainfo"><tbody><tr><td>';
  99. $output .= $this->parsers['general'][0]->output();
  100. if (isset($this->parsers['video'])) {
  101. $output .= '</td><td>';
  102. $output .= $this->parsers['video'][0]->output();
  103. }
  104. if (isset($this->parsers['audio'])) {
  105. $output .= '</td><td>';
  106. $output .= '<table><caption>Audio</caption><tbody>';
  107. foreach ($this->parsers['audio'] as $index => $ap) {
  108. $output .= sprintf('<tr><td>#%d: &nbsp;</td><td>%s</td></tr>', intval($index+1), $ap->output());
  109. }
  110. $output .= '</tbody></table>';
  111. }
  112. if (isset($this->parsers['text'])) {
  113. // Subtitles table will be printed below the previous table
  114. $output .= '<br />';
  115. $output .= '<table><caption>Subtitles</caption><tbody>';
  116. foreach ($this->parsers['text'] as $index => $tp) {
  117. $output .= sprintf('<tr><td>#%d: &nbsp;</td><td>%s</td></tr>', intval($index+1), $tp->output());
  118. }
  119. $output .= '</tbody></table>';
  120. }
  121. $output .= '</td></tr></tbody></table><br />';
  122. $output = $midiv_start . $string . $midiv_end . $output;
  123. if ($cummulative) {
  124. $output = $this->output . $output;
  125. }
  126. return $output;
  127. }
  128. public function output_raw()
  129. {
  130. $output = [];
  131. $sections = ['general', 'video', 'audio', 'text'];
  132. foreach ($sections as $section) {
  133. if (isset($this->parsers[$section])) {
  134. $output[$section] = [];
  135. foreach ($this->parsers[$section] as $index => $parser) {
  136. $output[$section][$index] = $parser->output_raw();
  137. }
  138. }
  139. }
  140. return $output;
  141. }
  142. // Strip escaped html tags
  143. // This is not done for security, just to beautify things (html should already be escaped)
  144. public static function strip_escaped_tags($string)
  145. {
  146. // use the php function first
  147. $string = strip_tags($string);
  148. $gt = '&gt;|&#62;|>';
  149. $lt = '&lt;|&#60;|<';
  150. // There is no opening tag, so don't go through the rest of the regexes
  151. if (!preg_match("($lt)", $string)) {
  152. return $string;
  153. }
  154. $tag_match = "/(?:$lt)(?P<tag>(?:(?!$gt).)*)(?:$gt)/ims";
  155. // This should match and remove tags
  156. $string = preg_replace($tag_match, '', $string);
  157. return $string;
  158. }
  159. protected function new_mediainfo()
  160. {
  161. $this->output .= $this->output(false);
  162. $this->parsed_lines = [];
  163. foreach (array_keys($this->parsers) as $key) {
  164. if ($key != static::GENERIC_PARSER) {
  165. unset($this->parsers[$key]);
  166. }
  167. }
  168. }
  169. }
  170. class SectionParser
  171. {
  172. protected $lines;
  173. protected $index;
  174. public function __construct(&$lines=[], &$i=0)
  175. {
  176. $this->set_lines($lines, $i);
  177. }
  178. public function set_lines(&$lines=[], &$i=0)
  179. {
  180. $this->lines = &$lines;
  181. $this->index = &$i;
  182. }
  183. // Should always return the read line
  184. public function parse_line()
  185. {
  186. if (!isset($this->lines[$this->index])) {
  187. return null;
  188. }
  189. $line = $this->lines[$this->index++];
  190. $pair = static::property_value_pair($line);
  191. $this->handle_cases($pair['property'], $pair['value']);
  192. return $line;
  193. }
  194. public function output()
  195. {
  196. }
  197. protected function handle_cases($property, $value)
  198. {
  199. }
  200. public static function section_name($string)
  201. {
  202. if (!$string) {
  203. return false;
  204. }
  205. $mistart="/^(?:general$|unique id|complete name)/i";
  206. if (preg_match($mistart, $string)) {
  207. return ParseManager::MEDIAINFO_START;
  208. }
  209. $words = explode(' ', $string);
  210. return strtolower($words[0]);
  211. }
  212. public static function property_value_pair($string)
  213. {
  214. $pair = explode(":", $string, 2);
  215. return array('property'=>strtolower(trim($pair[0])),'value'=>trim($pair[1]??''));
  216. }
  217. public static function strip_path($string)
  218. { // Remove filepath
  219. $string = str_replace("\\", "/", $string);
  220. $path_parts = pathinfo($string);
  221. return $path_parts['basename'];
  222. }
  223. public static function parse_size($string)
  224. {
  225. return str_replace(array('pixels', ' '), null, $string);
  226. }
  227. protected static function table_head($caption)
  228. {
  229. return "<table class='nobr'><caption>$caption</caption><tbody>";
  230. }
  231. protected static function table_row($property, $value)
  232. {
  233. if ($value) {
  234. return "<tr><td>$property:&nbsp;&nbsp;</td><td>$value</td></tr>";
  235. }
  236. return '';
  237. }
  238. protected static function table_tail()
  239. {
  240. return '</tbody></table>';
  241. }
  242. }
  243. class AudioSectionParser extends SectionParser
  244. {
  245. protected $audioformat;
  246. protected $audiobitrate;
  247. protected $audiochannels;
  248. protected $audiochannelpositions;
  249. protected $audiotitle;
  250. protected $audiolang;
  251. protected $audioprofile;
  252. protected $form_audioformat;
  253. protected $form_audiochannels;
  254. protected function handle_cases($property, $value)
  255. {
  256. switch ($property) {
  257. case "format":
  258. $this->audioformat = $value;
  259. break;
  260. case "bit rate":
  261. $this->audiobitrate = $value;
  262. break;
  263. case "channel(s)":
  264. $this->audiochannels = $value;
  265. break;
  266. case "channel positions":
  267. $this->audiochannelpositions = $value;
  268. break;
  269. case "title":
  270. $this->audiotitle = $value;
  271. break;
  272. case "language":
  273. $this->audiolang = $value;
  274. break;
  275. case "format profile":
  276. $this->audioprofile = $value;
  277. break;
  278. }
  279. }
  280. public function output()
  281. {
  282. $this->process_vars();
  283. $output = $this->audiolang . ' ' . $this->channels() . ' ' . $this->format();
  284. $output .= ($this->audiobitrate) ? " @ $this->audiobitrate" : '';
  285. $output .= ($this->audiotitle) ? " ($this->audiotitle)" : '';
  286. return $output;
  287. }
  288. public function output_raw()
  289. {
  290. $this->process_vars();
  291. $output = [];
  292. $properties = [
  293. 'audioformat', 'audiobitrate', 'audiochannels',
  294. 'audiochannelpositions', 'audiotitle', 'audiolang', 'audioprofile',
  295. 'form_audioformat', 'form_audiochannels'
  296. ];
  297. foreach ($properties as $property) {
  298. if ($this->$property) {
  299. $output[$property] = $this->$property;
  300. }
  301. }
  302. return $output;
  303. }
  304. protected function process_vars()
  305. {
  306. $this->form_audioformat = $this->form_format();
  307. $this->form_audiochannels = $this->form_channels();
  308. }
  309. protected function format()
  310. {
  311. if (strtolower($this->audioformat) === 'mpeg audio') {
  312. switch (strtolower($this->audioprofile)) {
  313. case 'layer 3':
  314. return 'MP3';
  315. case 'layer 2':
  316. return 'MP2';
  317. case 'layer 1':
  318. return 'MP1';
  319. }
  320. }
  321. return $this->audioformat;
  322. }
  323. protected function form_format()
  324. {
  325. // Not implemented: Real Audio, DTS-HD
  326. switch (strtolower($this->format())) {
  327. case 'mp2':
  328. return 'MP2';
  329. case 'mp3':
  330. return 'MP3';
  331. case 'vorbis':
  332. return 'OGG';
  333. case 'aac':
  334. return 'AAC';
  335. case 'ac-3':
  336. return 'AC3';
  337. case 'truehd':
  338. return 'TrueHD';
  339. case 'dts':
  340. switch (strtolower($this->audioprofile)) {
  341. case 'es':
  342. return 'DTS-ES';
  343. case 'ma / core':
  344. return 'DTS-HD MA';
  345. default:
  346. return 'DTS';
  347. }
  348. // no break
  349. case 'flac':
  350. return 'FLAC';
  351. case 'pcm':
  352. return 'PCM';
  353. case 'wma':
  354. return 'WMA';
  355. }
  356. }
  357. protected function channels()
  358. {
  359. if (isset($this->audiochannels)) {
  360. $chans = preg_replace('/^(\d).*$/', '$1', $this->audiochannels);
  361. if (isset($this->audiochannelpositions) && preg_match(
  362. '/LFE/',
  363. $this->audiochannelpositions
  364. )) {
  365. $chans -= .9;
  366. } else {
  367. $chans = $chans . '.0';
  368. }
  369. return $chans . 'ch';
  370. }
  371. }
  372. protected function form_channels()
  373. {
  374. return preg_replace('/ch/', '', $this->channels());
  375. }
  376. }
  377. class GeneralSectionParser extends SectionParser
  378. {
  379. public $filename;
  380. protected $generalformat;
  381. protected $duration;
  382. protected $filesize;
  383. protected $form_codec;
  384. protected $form_releasegroup;
  385. protected function handle_cases($property, $value)
  386. {
  387. switch ($property) {
  388. case "complete name":
  389. // Remove autodetected urls
  390. $value = preg_replace('#\[autourl(?:=.+)?\](.+)\[/autourl\]#', '$1', $value);
  391. $this->filename = static::strip_path($value);
  392. $this->lines[$this->index-1] = "Complete name : " . $this->filename;
  393. $this->filename = substr($this->filename, 0, 150);
  394. break;
  395. case "format":
  396. $this->generalformat = $value;
  397. break;
  398. case "duration":
  399. $this->duration = $value;
  400. break;
  401. case "file size":
  402. $this->filesize = $value;
  403. break;
  404. }
  405. }
  406. public function output()
  407. {
  408. $this->process_vars();
  409. $output = static::table_head('General');
  410. $properties = [
  411. 'Container' => 'generalformat',
  412. 'Runtime' => 'duration',
  413. 'Size' => 'filesize'
  414. ];
  415. foreach ($properties as $property => $value) {
  416. $output .= static::table_row($property, $this->$value);
  417. }
  418. $output .= static::table_tail();
  419. return $output;
  420. }
  421. public function output_raw()
  422. {
  423. $this->process_vars();
  424. $output = [];
  425. $properties = [
  426. 'filename', 'generalformat', 'duration', 'filesize', 'form_codec',
  427. 'form_releasegroup'
  428. ];
  429. foreach ($properties as $property) {
  430. if ($this->$property) {
  431. $output[$property] = $this->$property;
  432. }
  433. }
  434. return $output;
  435. }
  436. protected function process_vars()
  437. {
  438. switch (strtolower($this->generalformat)) {
  439. case 'mpeg-ts':
  440. $this->form_codec = 'MPEG-TS';
  441. break;
  442. // We can't determine if it's DVD5 or DVD9, so don't guess
  443. case 'mpeg-ps':
  444. $this->form_codec = '---';
  445. break;
  446. }
  447. $matches = [];
  448. preg_match(
  449. '/(?:^|.*\\|\/)\[(.*?)\].*$/',
  450. $this->filename,
  451. $matches
  452. );
  453. $this->form_releasegroup = $matches ? $matches[1] : '';
  454. }
  455. }
  456. class TextSectionParser extends SectionParser
  457. {
  458. protected $title;
  459. protected $language;
  460. protected $format;
  461. protected $default;
  462. protected $processed_language;
  463. protected $form_format;
  464. protected function handle_cases($property, $value)
  465. {
  466. switch ($property) {
  467. case 'title':
  468. $this->title = $value;
  469. break;
  470. case 'language':
  471. $this->language = $value;
  472. break;
  473. case 'format':
  474. $this->format = $value;
  475. break;
  476. case 'default':
  477. $this->default = ($value == 'Yes');
  478. break;
  479. }
  480. }
  481. public function output()
  482. {
  483. $this->process_vars();
  484. $language = $this->processed_language;
  485. $output = "$language ($this->format)";
  486. if ($this->title) {
  487. $output .= " ($this->title)";
  488. }
  489. if ($this->default) {
  490. $output .= ' (default)';
  491. }
  492. return $output;
  493. }
  494. public function output_raw()
  495. {
  496. $this->process_vars();
  497. $output = [];
  498. $properties = [
  499. 'title', 'language', 'format', 'default', 'processed_language',
  500. 'form_format'
  501. ];
  502. foreach ($properties as $property) {
  503. if ($this->$property) {
  504. $output[$property] = $this->$property;
  505. }
  506. }
  507. return $output;
  508. }
  509. protected function process_vars()
  510. {
  511. $this->processed_language = ($this->language) ?
  512. $this->language : 'Unknown';
  513. $this->form_format = 'Softsubbed';
  514. }
  515. }
  516. class VideoSectionParser extends SectionParser
  517. {
  518. protected $videoformat;
  519. protected $videoformatversion;
  520. protected $codec;
  521. protected $width;
  522. protected $height;
  523. protected $writinglibrary;
  524. protected $frameratemode;
  525. protected $framerate;
  526. protected $aspectratio;
  527. protected $bitrate;
  528. protected $bitratemode;
  529. protected $nominalbitrate;
  530. protected $bpp;
  531. protected $bitdepth;
  532. protected $processed_codec;
  533. protected $processed_resolution;
  534. protected $processed_framerate;
  535. protected $form_codec;
  536. protected $form_resolution;
  537. protected function handle_cases($property, $value)
  538. {
  539. switch ($property) {
  540. case "format":
  541. $this->videoformat = $value;
  542. break;
  543. case "format version":
  544. $this->videoformatversion = $value;
  545. break;
  546. case "codec id":
  547. $this->codec = strtolower($value);
  548. break;
  549. case "width":
  550. $this->width = static::parse_size($value);
  551. break;
  552. case "height":
  553. $this->height = static::parse_size($value);
  554. break;
  555. case "writing library":
  556. $this->writinglibrary = $value;
  557. break;
  558. case "frame rate mode":
  559. $this->frameratemode = $value;
  560. break;
  561. case "frame rate":
  562. // if variable this becomes Original frame rate
  563. $this->framerate = $value;
  564. break;
  565. case "display aspect ratio":
  566. $this->aspectratio = $value;
  567. break;
  568. case "bit rate":
  569. $this->bitrate = $value;
  570. break;
  571. case "bit rate mode":
  572. $this->bitratemode = $value;
  573. break;
  574. case "nominal bit rate":
  575. $this->nominalbitrate = $value;
  576. break;
  577. case "bits/(pixel*frame)":
  578. $this->bpp = $value;
  579. break;
  580. case 'bit depth':
  581. $this->bitdepth = $value;
  582. break;
  583. }
  584. }
  585. public function output()
  586. {
  587. $this->process_vars();
  588. $output = static::table_head('Video');
  589. $properties = [
  590. 'Codec' => 'processed_codec',
  591. 'Bit depth' => 'bitdepth',
  592. 'Resolution' => 'processed_resolution',
  593. 'Aspect ratio' => 'aspectratio',
  594. 'Frame rate' => 'processed_framerate',
  595. 'Bit rate' => 'bitrate',
  596. 'BPP' => 'bpp'
  597. ];
  598. foreach ($properties as $property => $value) {
  599. $output .= static::table_row($property, $this->$value);
  600. }
  601. $output .= static::table_tail();
  602. return $output;
  603. }
  604. public function output_raw()
  605. {
  606. $this->process_vars();
  607. $output = [];
  608. $properties = [
  609. 'videoformat', 'videoformatversion', 'codec', 'width', 'height',
  610. 'writinglibrary', 'frameratemode', 'framerate', 'aspectratio',
  611. 'bitrate', 'bitratemode', 'nominalbitrate', 'bpp', 'bitdepth',
  612. 'processed_codec', 'processed_resolution', 'processed_framerate',
  613. 'form_codec', 'form_resolution'
  614. ];
  615. foreach ($properties as $property) {
  616. if ($this->$property) {
  617. $output[$property] = $this->$property;
  618. }
  619. }
  620. return $output;
  621. }
  622. protected function process_vars()
  623. {
  624. $this->processed_codec = $this->compute_codec();
  625. $this->processed_resolution = ($this->width) ?
  626. $this->width . 'x' . $this->height : '';
  627. $this->processed_framerate = (strtolower($this->frameratemode) !=
  628. "constant" && $this->frameratemode) ?
  629. $this->frameratemode : $this->framerate;
  630. $this->form_codec = $this->compute_form_codec();
  631. $this->form_resolution = $this->compute_form_resolution();
  632. }
  633. protected function compute_codec()
  634. {
  635. switch (strtolower($this->videoformat)) {
  636. case "mpeg video":
  637. switch (strtolower($this->videoformatversion)) {
  638. case "version 2":
  639. return "MPEG-2";
  640. case "version 1":
  641. return "MPEG-1";
  642. }
  643. return $this->videoformat;
  644. }
  645. switch (strtolower($this->codec)) {
  646. case "div3":
  647. return "DivX 3";
  648. case "divx":
  649. case "dx50":
  650. return "DivX";
  651. case "xvid":
  652. return "XviD";
  653. case "x264":
  654. return "x264";
  655. }
  656. $chk = strtolower($this->codec);
  657. $wl = strtolower($this->writinglibrary);
  658. if (($chk === "v_mpeg4/iso/avc" || $chk === "avc1") && strpos($wl, "x264 core") === false) {
  659. return "H264";
  660. } elseif (($chk === "v_mpeg4/iso/avc" || $chk === "avc1") && strpos($wl, "x264 core") > -1) {
  661. return "x264";
  662. } elseif (strtolower($this->videoformat) === "avc" && strpos($wl, "x264 core") === false) {
  663. return "H264";
  664. }
  665. if (($chk === 'v_mpegh/iso/hevc') || ($wl === 'hevc')) {
  666. return 'H265';
  667. }
  668. }
  669. protected function compute_form_codec()
  670. {
  671. // Not implemented: DVD5, DVD9, WMV, Real Video
  672. // MPEG-TS set as GeneralSectionParser::$form_codec if found
  673. // MPEG-PS sets GeneralSectionParser::$form_codec to blank form value
  674. // so DVD5 or DVD9 is selected manually.
  675. $codec = $this->compute_codec();
  676. switch (strtolower($codec)) {
  677. case 'x264':
  678. case 'h264':
  679. return strtolower($this->bitdepth) == '10 bits' ? 'h264 10-bit' : 'h264';
  680. case 'h265':
  681. return 'h265';
  682. case 'xvid':
  683. return 'XviD';
  684. case 'divx':
  685. case 'divx 3':
  686. return 'DivX';
  687. case 'mpeg-1':
  688. return 'MPEG';
  689. case 'mpeg-2':
  690. return 'MPEG-2';
  691. }
  692. switch (strtolower($this->codec)) {
  693. case 'wmv3':
  694. return 'VC-1';
  695. case 'mp43':
  696. return 'MPEG-4 v3';
  697. }
  698. switch (strtolower($this->videoformat)) {
  699. case 'vc-1':
  700. return 'VC-1';
  701. case 's-mpeg 4 v3':
  702. return 'MPEG-4 v3';
  703. }
  704. }
  705. protected function compute_form_resolution()
  706. {
  707. global $Resolutions;
  708. $closest = null;
  709. if (isset($this->height)) {
  710. $resolutions = $Resolutions;
  711. foreach ($resolutions as $resolution) {
  712. if (!isset($closest) || abs($this->height - $resolution) <
  713. abs($this->height - $closest)) {
  714. $closest = $resolution;
  715. }
  716. }
  717. }
  718. return $closest;
  719. }
  720. }