This page lists files in the current directory. You can view content, get download/execute commands for Wget, Curl, or PowerShell, or filter the list using wildcards (e.g., `*.sh`).
wget 'https://sme10.lists2.roe3.org/kodbox/plugins/fileThumb/lib/KodImageMagick.class.php'
<?php
/**
* 图片处理类-ImageMagick命令行
*/
class KodImageMagick {
private $plugin;
private $command;
public function __construct($plugin) {
$this->plugin = $plugin;
}
// 格式是否支持
public function isSupport($ext) {
return true;
}
// 获取命令
public function getCommand($type='convert') {
return $this->plugin->getCommand($type);
}
/**
* 图片生成缩略图
* @param [type] $file
* @param [type] $cacheFile
* @param [type] $maxSize
* @param [type] $ext
* @return void
*/
// imagemagick -density 100 //耗时间,暂时去除
// convert -density 900 banner.psd -colorspace RGB -resample 300 -trim -thumbnail 200x200 test.jpg
// convert -colorspace rgb simple.pdf[0] -density 100 -sample 200x200 sample.jpg
public function createThumb($file, $cacheFile, $maxSize, $ext) {
if (!file_exists($file) || !$this->isSupport($ext)) return false;
$command = $this->getCommand();
if (!$command) return false;
// 生成缩略图,不能比原来大;
$sizeInfo = $this->getImgSize($file,$ext);
if($sizeInfo && is_array($sizeInfo)){
$sizeMin = min($sizeInfo[0],$sizeInfo[1]);
$maxSize = $sizeMin ? min($sizeMin,$maxSize): 250;
}
$size = $maxSize.'x'.$maxSize;
$param = "-auto-orient -alpha off -quality 90 -size ".$size;
$tempName = rand_string(15).'.png';
switch ($ext){
case 'eps':
case 'psb':
case 'psd':
case 'ps'://ps,ai,pdf; ==> window:缺少组件;mac:命令行可以执行,但php执行有问题
case 'ai':$file.= '[0]';break;
case 'pdf':$file.= '[0]';
$param = "-auto-orient -alpha remove -alpha off -quality 90 -size ".$size." -background white";
break; // pdf 生成缩略图透明背景变为黑色问题处理;
/**
* 生成doc/docx封面; or转图片;
*
* mac: 使用liboffice=>soffice 关联convert的delegate;
* centos : unoconv (yum install unoconv);
* 转doc/docx/ppt/pptx/xls/xlsx/odt/odf为pdf; 再用convert提取某页为图片;
* 实现office预览方案之一;(中文字体copy)
* unoconv -f pdf /data/from.docx /data/toxx.pdf;
* // https://github.com/ScoutsGidsenVL/Pydio/blob/master/plugins/editor.imagick/class.IMagickPreviewer.php
*/
case 'ppt':
case 'pptx':
case 'doc':
case 'docx':$file.= '[0]';break;
// https://legacy.imagemagick.org/Usage/thumbnails/
case 'tif':$file.= '[0]';$param .= " -flatten ";break;
//case 'gif':$file.= '[0]';break;
case 'gif':
$param = "-thumbnail ".$size;
$tempName = rand_string(15).'.gif';
break;
case 'webp':
case 'png':
case 'bmp':$param = "-resize {$size}";break;
case 'jpe':
case 'jpg':
case 'jpeg':
case 'avif':
case 'heic':$param = "-resize {$size} -auto-orient";break;
default:
$dng = 'dng,cr2,erf,raf,kdc,dcr,mrw,nrw,nef,orf,pef,x3f,srf,arw,sr2';
$imageExt = $dng.',3fr,crw,dcm,fff,iiq,mdc,mef,mos,plt,ppm,raw,rw2,srw,tst';
if(in_array($ext,explode(',',$imageExt))){
$param = "-resize {$size}";
//$file = 'rgb:'.$file.'[0]';
}
break;
}
// 移除元数据、最低压缩级别、禁止过滤,8位深度——可能执行快一点,内存占用没有区别
if ($ext != 'gif') {
$param .= ' -strip -define png:compression-level=1 -define png:filter=0 -depth 8';
}
$param = $this->plugin->memLimitParam('convert', $param); // 加上内存限制
$tempPath = $this->getTmpPath($cacheFile, $tempName);
$this->setLctype($file,$tempPath);
$script = $command.' '.$param.' '.escapeShell($file).' '.escapeShell($tempPath).' 2>&1';
$script = "export MAGICK_THREAD_LIMIT=2; {$script}"; // 限制进程数
$out = shell_exec($script);
if(!file_exists($tempPath)) return $this->log('image thumb error:'.$out.';cmd='.$script);
move_path($tempPath,$cacheFile);
return true;
}
/**
* 使用ffmpeg生成视频封面
* @param [type] $file
* @param [type] $cacheFile
* @param [type] $videoThumbTime
* @return void
*/
public function createThumbVideo($file,$cacheFile,$videoThumbTime){
$command = $this->getCommand('ffmpeg');
if (!$command) return false;
$tempPath = $this->getTmpPath($cacheFile, rand_string(15).'.jpg');
$maxWidth = 800;
$timeAt = $videoThumbTime ? ' -ss 00:00:03' : ''; // 截取时间点,前置(ffmpeg -ss 00:00:03)可直接从第3秒开始处理,提升效率
$this->setLctype($file,$tempPath);
$script = $this->plugin->memLimitParam('ffmpeg', $command) . $timeAt . ' -i '.escapeShell($file).' -y -f image2 -vframes 1 '.escapeShell($tempPath).' 2>&1';
// $script = "/usr/bin/time -v ".$script; // Maximum resident set size
$out = shell_exec($script);
if(!file_exists($tempPath)) {
$this->log('video thumb error,'.$out.';cmd='.$script);
return false;
}
move_path($tempPath,$cacheFile);
$cm = new ImageThumb($cacheFile,'file');
$cm->prorate($cacheFile,$maxWidth,$maxWidth);
return true;
}
/**
* 获取图片信息,类getimagesize格式
*/
public function getImgSize($file, $ext = '') {
if (!file_exists($file)) return false;
// $res = shell_exec("identify -format '%wx%hx%[channels]x%[depth]' ".escapeshellarg($file));
$res = shell_exec("identify -format '%w %h' -ping ".escapeshellarg($file));
if (!$res) return false;
list($width, $height) = explode(' ', trim($res));
return array(
intval($width), // width
intval($height), // height
'channels' => 3,
'bits' => 8,
);
}
// 设置地区字符集,避免中文被过滤
private function setLctype($path,$path2=''){
if (stripos(setlocale(LC_CTYPE, 0), 'utf-8') !== false) return;
if (Input::check($path,'hasChinese') || ($path2 && Input::check($path2,'hasChinese'))) {
setlocale(LC_CTYPE, "en_US.UTF-8");
}
}
// 重置临时文件位置:linux下$cacheFile不可写问题,先生成到/tmp下;再移动出来
private function getTmpPath($tempFile, $tempName) {
$tempPath = $tempFile;
if($GLOBALS['config']['systemOS'] == 'linux' && is_writable('/tmp/')){
mk_dir('/tmp/fileThumb');
if(is_writable('/tmp/fileThumb')){ // 可能有不可写的情况;
$tempPath = '/tmp/fileThumb/'.$tempName;
}
}
return $tempPath;
}
// 记录日志
public function log($msg) {
$this->plugin->log($msg);
}
}
wget 'https://sme10.lists2.roe3.org/kodbox/plugins/fileThumb/lib/KodImagick.class.php'
<?php
/**
* 图片处理类-Imagick扩展
*/
class KodImagick {
// 支持的图像格式
private const IMAGE_FORMATS = array(
'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'tif', 'jpe', 'heic','avif'
);
// 支持的文档格式
private const DOCUMENT_FORMATS = array(
'psd', 'psb', 'eps', 'ai', 'pdf',
// 'doc', 'docx', 'ppt', 'pptx',
);
// 支持的相机RAW格式
private const RAW_FORMATS = array(
'dng', 'cr2', 'erf', 'raf', 'kdc', 'dcr', 'mrw', 'nrw', 'nef', 'orf', 'pef',
'x3f', 'srf', 'arw', 'sr2', '3fr', 'crw', 'dcm', 'fff', 'iiq', 'mdc', 'mef',
'mos', 'plt', 'ppm', 'raw', 'rw2', 'srw', 'tst'
);
// 所有支持格式
private $allFormats = array();
private $plugin;
private $defQuality = 85;
private $defFormat = 'jpeg';
private $maxResolution = 40000; // 支持的最大分辨率
public function __construct($plugin) {
$this->plugin = $plugin;
$this->allFormats = array_merge(
self::IMAGE_FORMATS,
self::DOCUMENT_FORMATS,
self::RAW_FORMATS
);
$this->setTmpDir();
$this->setMemLimit();
}
// 设置Imagick临时目录
private function setTmpDir() {
if(!is_dir(TEMP_FILES)){mk_dir(TEMP_FILES);}
$path = TEMP_FILES . '/imagick'; mk_dir($path);
putenv('MAGICK_TEMPORARY_PATH='.$path);
putenv('MAGICK_TMPDIR='.$path);
}
// 设置Imagick内存限制——实际是ImageMagick在占用系统内存,不受PHP内存限制
private function setMemLimit() {
$memFree = $this->getMemLimit();
Imagick::setResourceLimit(Imagick::RESOURCETYPE_MEMORY, $memFree);
Imagick::setResourceLimit(Imagick::RESOURCETYPE_MAP, $memFree * 2);
}
// 获取系统内存限制
private function getMemLimit() {
$memBase = 128 * 1024 * 1024; // 128M,最小内存限制——实际可用内存可能更小,暂不处理
$memFree = $this->plugin->sysMemoryFree();
return max($memBase, intval($memFree * 0.5));
}
// 格式是否支持
public function isSupport($ext) {
return in_array(strtolower($ext), $this->allFormats);
}
/**
* 图片生成缩略图
* @param [type] $file
* @param [type] $cacheFile
* @param [type] $maxSize
* @param [type] $ext
* @return void
*/
public function createThumb($file, $cacheFile, $maxSize, $ext) {
if (!file_exists($file) || !$this->isSupport($ext)) return false;
$this->setMemLimit(); // 内存限制
$imagick = null;
try {
$imagick = new Imagick();
// 预读图像尺寸
$imagick->pingImage($file);
$orgWidth = $imagick->getImageWidth();
$orgHeight = $imagick->getImageHeight();
$imagick->clear(); // 清除ping结果
// 限制超大图片
if ($orgWidth > $this->maxResolution || $orgHeight > $this->maxResolution) {
$msg = sprintf("Imagick convert error [%s]: Image too large: %s x %s", $file, $orgWidth, $orgHeight);
$this->log($msg);
return false;
}
// 启用像素缓存加速
// $imagick->setOption('temporary-path', '/dev/shm');
$imagick->setOption('cache:sync', '0'); // 禁用同步写入
$imagick->setOption('sampling-factor', '4:2:0'); // 色度子采样
$imagick->setOption('filter:blur', '0.8'); // 轻微模糊提升缩放速度
// 读取图像
if (in_array($ext, self::DOCUMENT_FORMATS)) {
// 特殊格式处理
$imagick->setResolution(300, 300);
$imagick->readImage($file . '[0]');
} else if (in_array($ext, self::RAW_FORMATS)) {
// RAW格式处理
$imagick->setResolution(300, 300);
$imagick->readImage($file);
$this->correctImageOrientation($imagick);
} else if ($ext === 'gif' || $ext === 'tif') {
// 多帧图像处理
$imagick->readImage($file . '[0]');
} else {
// 超大图片,读取后立即缩小
$maxReadSize = 5000; // 预读最大尺寸
if ($orgWidth > $maxReadSize || $orgHeight > $maxReadSize) {
$ratio = min($maxReadSize / $orgWidth, $maxReadSize / $orgHeight);
$readWidth = intval($orgWidth * $ratio);
$readHeight = intval($orgHeight * $ratio);
// $imagick->sampleImage($readWidth, $readHeight); // 先降采样
$imagick->readImage($file . '[' . $readWidth . 'x' . $readHeight . ']');
$imagick->scaleImage($readWidth, $readHeight, true); // 缩放
} else {
// 普通图像
$imagick->readImage($file);
}
}
// 获取实际图像(处理多页文档)
$image = $imagick->getImage();
// 自动旋转校正
$this->autoOrientImage($image);
// 颜色空间转换
if ($image->getImageColorspace() == Imagick::COLORSPACE_CMYK) {
$image->transformImageColorspace(Imagick::COLORSPACE_SRGB);
}
// 保持宽高比缩放
$image->thumbnailImage($maxSize, 0);
// $ratio = $maxSize / $orgWidth; // 缩略图的宽度比例
// $newHeight = (int)($orgHeight * $ratio);
// $image->resizeImage($maxSize, $newHeight, Imagick::FILTER_LANCZOS, 1);
// $image->thumbnailImage($maxSize, 0, true, false); // 报错
// 处理透明背景(当输出格式为JPEG时)
if ($this->defFormat === 'jpeg' && $image->getImageAlphaChannel()) {
$thumbWidth = $image->getImageWidth();
$thumbHeight = $image->getImageHeight();
$background = new Imagick();
$background->newImage(
$thumbWidth,
$thumbHeight,
'white',
'jpeg'
);
$background->compositeImage(
$image,
Imagick::COMPOSITE_OVER,
0, 0
);
$image = $background;
}
// 设置输出格式和质量
$image->setImageFormat($this->defFormat);
if ($this->defFormat === 'png') {
// PNG压缩级别:0-9 (0=无压缩, 9=最大压缩)
$compressionLevel = min(9, max(0, round(9 - ($this->defQuality / 10))));
$image->setOption('png:compression-level', $compressionLevel);
$image->setOption('png:exclude-chunk', 'all'); // 移除所有辅助块
$image->setOption('png:compression-strategy', '1'); // 更快策略
} else {
$image->setImageCompressionQuality($this->defQuality);
}
// 移除元数据
$image->stripImage();
// 写入文件
$result = $image->writeImage($cacheFile);
return $result;
} catch (Exception $e) {
// Stack trace: $e->getTraceAsString()
$msg = sprintf("Imagick convert error [%s]: %s", $file, $e->getMessage());
$this->log($msg);
return false;
} finally {
// 确保资源释放
if ($imagick instanceof Imagick) {
$imagick->clear();
$imagick->destroy();
}
}
}
/**
* 自动校正图像方向
*/
private function autoOrientImage(Imagick $image) {
$orientation = $image->getImageOrientation();
switch ($orientation) {
case Imagick::ORIENTATION_BOTTOMRIGHT:
$image->rotateImage('#000', 180);
break;
case Imagick::ORIENTATION_RIGHTTOP:
$image->rotateImage('#000', 90);
break;
case Imagick::ORIENTATION_LEFTBOTTOM:
$image->rotateImage('#000', -90);
break;
}
$image->setImageOrientation(Imagick::ORIENTATION_TOPLEFT);
}
/**
* 校正RAW格式图像方向
*/
private function correctImageOrientation(Imagick $image) {
try {
// 尝试获取EXIF方向信息
$exif = $image->getImageProperties('exif:*');
if (isset($exif['exif:Orientation'])) {
$orientation = (int)$exif['exif:Orientation'];
$image->setImageOrientation($orientation);
$this->autoOrientImage($image);
}
} catch (Exception $e) {
// 忽略EXIF读取错误
}
}
/**
* 获取图片信息,类getimagesize格式
*/
public function getImgSize($file, $ext = '') {
if (!file_exists($file)) return false;
$imagick = null;
try {
$imagick = new Imagick();
// 特殊格式只读第一页/第一帧
if (in_array($ext, self::DOCUMENT_FORMATS) || $ext === 'gif' || $ext === 'tif') {
$imagick->pingImage($file . '[0]');
} else {
$imagick->pingImage($file); // readImage,使用pingImage避免加载像素
}
// $image = $imagick->getImage();
return array (
$imagick->getImageWidth(),
$imagick->getImageHeight(),
// 'channels' => $image->getImageChannelCount(),
'channels' => 3,
'bits' => $imagick->getImageDepth(),
);
} catch (Exception $e) {
return false;
} finally {
if ($imagick instanceof Imagick) {
$imagick->clear();
$imagick->destroy();
}
}
}
// 记录日志
public function log($msg) {
$this->plugin->log($msg);
}
}
wget 'https://sme10.lists2.roe3.org/kodbox/plugins/fileThumb/lib/KodImaginary.class.php'
<?php
/**
* 图片处理类-Imaginary 服务
* https://github.com/h2non/imaginary
*/
class KodImaginary {
private $imgFormats = array(
// 'bmp' => 'image/(bmp|x-bitmap)', // Unsupported media type
'png' => 'image/png',
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'gif' => 'image/gif', // 支持,但没必要
'heic' => 'image/heic',
'heif' => 'image/heif',
'tif' => 'image/tiff',
'tiff' => 'image/tiff',
'webp' => 'image/webp',
// 'svg' => 'image/svg+xml',
'pdf' => 'application/pdf',
'ai' => 'application/illustrator'
);
private $plugin;
private $apiUrl;
private $apiKey;
private $urlKey;
private $defQuality = 85;
private $defFormat = 'jpeg'; // jpg不支持;png支持,但大小为jpeg的10+倍
public function __construct($plugin){
$this->plugin = $plugin;
$this->initData();
}
// 初始化服务参数
private function initData(){
$config = $this->plugin->getConfig();
$this->apiUrl = rtrim($config['imgnryHost'], '/');
$this->apiKey = $config['imgnryApiKey']; // -key
$this->urlKey = $config['imgnryUrlKey']; // -url-signature-key
}
// 检查服务状态
public function status(){
// $this->initData(); // 刷新配置参数
$rest = $this->imgRequest('/health', array());
return $rest ? true : false;
}
// 格式是否支持
public function isSupport($ext){
return isset($this->imgFormats[strtolower($ext)]);
}
/**
* 图片生成缩略图
* @param [type] $file
* @param [type] $cacheFile
* @param [type] $maxSize
* @param [type] $ext
* @return void
*/
public function createThumb($file,$cacheFile,$maxSize,$ext) {
if (request_url_safe($file)) {
$url = $file;
} else {
if (!file_exists($file)) {return false;}
}
if (!$this->isSupport($ext)) return false;
$this->image = $file;
$data = array(
'width' => $maxSize,
'height' => 0, // 关键:高度设为 0 触发自适应
'type' => $this->defFormat, // 可选:输出格式(默认保持原格式)
// 'quality' => $this->defQuality, // 可选:质量(默认自动)
// 'smartcrop' => 'true', // 智能裁剪(默认false),结合/thumbnail使用
// 'nocrop' => 'true', // 禁用裁剪(默认false)
// 'norotation' => 'true', // 禁用自动旋转(默认false)
'stripmeta' => 'true', // 去除元数据
'trace' => 'true',
'debug' => 'true',
);
$post = array('file' => '@'.$file);
if (isset($url)) {
$post = array('url' => $this->parsePathUrl($url)); // 网络文件,需启用 -enable-url-source
}
$path = '/resize'; // 精确控制比例
// $path = '/pipeline'; // 为图像执行系列操作,形成一个处理管道
// $path = '/thumbnail'; // 方形/固定比例;/thumbnail+smartcrop=true
$content = $this->imgRequest($path, $data, $post, $ext);
if (!$content) return false;
file_put_contents($cacheFile, $content);
return true;
}
/**
* 获取图片信息,类getimagesize格式
* @param [type] $file
* @return void
*/
public function getImgSize($file, $ext='') {
if (request_url_safe($file)) {
$url = $file;
} else if (file_exists($file)) {
$url = Action('explorer.share')->link($file); // localhost/127不支持访问,暂不处理
}
if (!$url) {return false;}
if ($ext && !$this->isSupport($ext)) return false;
$this->image = $file;
// 需要是imaginary能访问的url
$post = array('url' => $this->parsePathUrl($url));
$info = $this->imgRequest('/info', array(), $post, $ext);
if (!$info) return false;
return array(
$info['width'],
$info['height'],
'channels' => _get($info,'channels',4),
'bits' => 8,
// 'mime' => 'image/'.$info['type'],
);
}
/**
* 图片处理请求
* @param [type] $path
* @param [type] $data
* @param boolean $post
* @return void
*/
private function imgRequest($path, $data, $post=array(), $ext='') {
// key必须作为url参数传递,否则报错:Invalid or missing API key
if (!empty($this->apiKey)) {
$data['key'] = $this->apiKey;
}
$method = 'POST';
if (isset($post['url'])) {
$method = 'GET';
$data['url'] = $post['url'];
$post = array();
}
$query = http_build_query($data);
$query = $this->signUrl($path, $query);
$url = $this->apiUrl . $path . '?' . $query;
$rest = url_request($url, $method, $post);
// pr($url,$rest,$ext,$post);exit;
if(!$rest || !isset($rest['data'])){
$this->log("imaginary {$path} error: [{$this->image}] request failed");
return false;
}
$data = json_decode($rest['data'],true);
if (!$rest['status'] || $rest['code'] != 200) {
$msg = _get($data, 'message', 'unknown error');
$this->log("imaginary {$path} error: [{$this->image}] " . $msg);
return false;
}
return $data ? $data : $rest['data'];
}
/**
* 为Imaginary URL生成签名
*/
private function signUrl($path, $query) {
if (empty($this->urlKey)) {return $query;}
$toSign = $path . '?' . $query;
$signature = base64_encode(hash_hmac('sha1', $toSign, $this->urlKey, true));
$signature = rtrim(strtr($signature, '+/', '-_'), '=');
return $query . '&signature=' . urlencode($signature);
}
// 记录日志
public function log($msg) {
$this->plugin->log($msg);
}
// 非同一服务器localhost访问自动适配;后续可增加外网访问适配
public function parsePathUrl($url) {
$parse1 = parse_url($url);
$parse2 = parse_url($this->apiUrl);
if ($parse1['host'] != $parse2['host']) {
if ($parse1['host'] == 'localhost') {
$server = new ServerInfo();
$ip = method_exists($server, 'getInternalIP') ? $server->getInternalIP() : false; // get_server_ip();
if ($ip) {
$port = $parse1['port'] && $parse1['port'] != 80 ? ':' . $parse1['port'] : '';
$url = $parse1['scheme'] . '://' . $ip . $port . $parse1['path'] . (isset($parse1['query']) ? '?' . $parse1['query'] : '');
}
}
}
return $url;
}
}
wget 'https://sme10.lists2.roe3.org/kodbox/plugins/fileThumb/lib/TaskConvert.class.php'
<?php
/*
* @link http://kodcloud.com/
* @author warlee | e-mail:kodcloud@qq.com
* @copyright warlee 2014.(Shanghai)Co.,Ltd
* @license http://kodcloud.com/tools/license/license.txt
*/
/**
* 转码任务处理;
*/
class TaskConvert extends Task{
public function onKillSet($call,$args=array()){
$this->onKillCall = array($call,$args);
}
public function onKill(){
if(!$this->onKillCall) return;
ActionApply($this->onKillCall[0],$this->onKillCall[1]);
$this->onKillCall = false;
}
protected function endAfter(){
$this->onKill();
}
}
wget 'https://sme10.lists2.roe3.org/kodbox/plugins/fileThumb/lib/VideoResize.class.php'
<?php
/**
* 视频转码处理
*
* 转码条件: 视频文件格式,已解析到播放时长,大于20M,非系统文件
* 任务管理: 转码后进入后台执行,旁路定时获取转码进度,更新到任务; 任务可手动结束,结束后同时结束转码进程;
* 转码配置: 开启转码,最大并发进程,最小文件大小;(任务列表)
* 并发管理: 进行中则返回进度; 添加时检测允许中的任务,大于最大限制则直接返回信息;
* 播放处理: 开启了转码时,播放时请求该文件标清视频(已完成返回标清视频链接;首次触发转码;进行中则获取进度;任务繁忙情况);
*
* test: http://127.0.0.1/kod/kodbox/?plugin/fileThumb/videoSmall&path={source:3801668}
* 视频压制ffmpeg参数: https://www.bilibili.com/read/cv5804639/
* ffmpeg参数说明: https://github.com/fujiawei-dev/ffmpeg-generator/blob/master/docs/vfilters.md
* 部分服务器处理视频转码绿色条纹花屏问题(兼容处理)
* 图片缩放指定转码算法 -sws_flags, 默认为bilinear, ok=accurate_rnd,neighbor,bitexact(neighbor会有锯齿)
* https://www.sohu.com/a/551562728_121066035
* https://www.cnblogs.com/acloud/archive/2011/10/29/sws_scale.html
* https://github.com/hemanthm/ffmpeg-soc/blob/376d5c5f13/libswscale/options.c
* https://blog.csdn.net/Randy009/article/details/51523331
*/
class videoResize {
const STATUS_SUCCESS = 2; //成功;
const STATUS_IGNORE = 3; //忽略转换,大小格式等不符合;
const STATUS_ERROR = 4; //转换失败,ffmepg不存在等原因;
const STATUS_RUNNING = 5; //转换进行中;
const STATUS_LIMIT = 6; //任务数达到上限,忽略该任务.
public function start($plugin){
$path = $plugin->filePath($GLOBALS['in']['path']);
$fileInfo = IO::info($path);
$fileInfo = Action('explorer.list')->pathInfoMore($fileInfo);
$fileInfo['taskID'] = 'video-convert-'.KodIO::hashPath($fileInfo);
$status = $this->run($path,$fileInfo,$plugin);
if($status === true) return;
$taskID = $fileInfo['taskID'];
$result = array('status'=>$status,'taskRun'=>Cache::get('fileThumb-videoResizeCount'));
switch ($status) {
case self::STATUS_SUCCESS:
$findSource = IO::fileNameExist($plugin->cachePath,$taskID.".mp4");
$sourcePath = KodIO::make($findSource);
$result['msg'] = LNG('fileThumb.video.STATUS_SUCCESS');
$result['data'] = $plugin->pluginApi.'videoSmall&play=1&path='.rawurlencode($path);
$result['data'] = Action('explorer.share')->link($sourcePath);
$result['size'] = array(
'before' => size_format($fileInfo['size']),
'now' => size_format(IO::size($sourcePath)),
);
// if($_GET['play'] == '1'){IO::fileOut($sourcePath);exit;}
break;
case self::STATUS_IGNORE:$result['msg'] = LNG('fileThumb.video.STATUS_IGNORE');break;
case self::STATUS_ERROR:$result['msg'] = $this->convertError($taskID);break;
case self::STATUS_RUNNING:
$result['msg'] = LNG('fileThumb.video.STATUS_RUNNING');
$result['data'] = Task::get($taskID);
$errorCheck = 'Task_error_'.$taskID;
if(!$result['data']){
$lastError = Cache::get($errorCheck);
if(!$lastError){Cache::set($errorCheck,time(),60);}
if($lastError && time() - $lastError > 10){
$this->convertClear($taskID);
Cache::remove($errorCheck);
}
}else{
Cache::remove($errorCheck);
}
break;
case self::STATUS_LIMIT:$result['msg'] = LNG('fileThumb.video.STATUS_LIMIT');break;
default:break;
}
show_json($result);
}
public function run($path,$fileInfo,$plugin){
// io缩略图已存在,直接输出
$cachePath = $plugin->cachePath;
$taskID = $fileInfo['taskID'];
$tempFileName = $taskID.".mp4";
$config = $plugin->getConfig('fileThumb');
$isVideo = in_array($fileInfo['ext'],explode(',',$config['videoConvertType']));
$fileSizeMax = floatval($config['videoConvertLimitTo']); //GB; 为0则不限制
$fileSizeMin = floatval($config['videoConvertLimit']); //GB; 为0则不限制
if(IO::fileNameExist($cachePath, $tempFileName)){return self::STATUS_SUCCESS;}
// 部分文件无法获取视频信息(或时长),导致无法执行转码
if ($isVideo) {
if (!isset($fileInfo['fileInfoMore'])) $fileInfo['fileInfoMore'] = array();
if (!isset($fileInfo['fileInfoMore']['playtime'])) $fileInfo['fileInfoMore']['playtime'] = 0;
}
$pathDisplay = isset($fileInfo['pathDisplay']) ? $fileInfo['pathDisplay'] : $fileInfo['path'];
if( !$isVideo ||
!is_array($fileInfo['fileInfoMore']) ||
!isset($fileInfo['fileInfoMore']['playtime']) ||
strstr($pathDisplay,'/systemPath/systemTemp/plugin/fileThumb/') ||
strstr($pathDisplay,'/tmp/fileThumb') ||
strstr($pathDisplay,TEMP_FILES) ||
($fileSizeMax > 0.01 && $fileInfo['size'] > 1024 * 1024 * 1024 * $fileSizeMax) ||
($fileSizeMin > 0.01 && $fileInfo['size'] < 1024 * 1024 * $fileSizeMin) ){
return self::STATUS_IGNORE;
}
if($this->convertError($taskID)){return self::STATUS_ERROR;} // 上次转换失败缓存记录;
$command = $plugin->getFFmpeg();
if(!$command || !function_exists('proc_open') || !function_exists('shell_exec')){
$this->convertError($taskID,LNG('fileThumb.video.STATUS_ERROR').'(3501)',600);
return self::STATUS_ERROR;//Ffmpeg 软件未找到,请安装后再试
}
if(!$this->convertSupport($command)){
$this->convertError($taskID,'ffmpeg not support libx264; please repeat install'.'(3502)',600);
return self::STATUS_ERROR;//Ffmpeg 转码解码器不支持;
}
// 过短的视频封面图,不指定时间;
$localFile = $plugin->localFile($path);
if(Cache::get($taskID) == 'error'){return self::STATUS_ERROR;}; //是否已经转码
if(!$localFile ){
Cache::set($taskID,'error',60);
$this->convertError($taskID,'localFile move error!'.'(3503)',60);
return self::STATUS_IGNORE;
}
$tempPath = TEMP_FILES.$tempFileName;
if($GLOBALS['config']['systemOS'] == 'linux' && is_writable('/tmp/')){
$tempPath = '/tmp/fileThumb/'.$tempFileName;
}
mk_dir(get_path_father($tempPath));
if(Cache::get($taskID) == 'running') return self::STATUS_RUNNING;
$runTaskMax = intval($config['videoConvertTask']);
$runTaskCount = intval(Cache::get('fileThumb-videoResizeCount'));
if($runTaskCount >= $runTaskMax) return self::STATUS_LIMIT;
if($_GET['noOutput'] == '1'){http_close();} // 默认输出;
$this->convertAdd($taskID);
// https://www.ruanyifeng.com/blog/2020/01/ffmpeg.html
ignore_timeout();
$quality = 'scale=-2:480 -b:v 1000k -maxrate 1000k -sws_flags accurate_rnd';//480:-2 -2:480 限制码率; 缩放图片处理
$timeStart = time();// 画质/速度: medium/ultrafast/fast
$logFile = $tempPath.'.log';@unlink($logFile);//-vf scale=480:-2 -b:v 1024k -maxrate 1000k -threads 2
$args = '-c:a aac -preset medium -vf '.$quality.' -strict -2 -c:v libx264 1>'.$logFile.' 2>&1';
$script = $plugin->memLimitParam('ffmpeg', $command).' -y -i "'.$localFile.'" '.$args.' "'.$tempPath.'"';
// 后台运行
if($GLOBALS['config']['systemOS'] == 'windows'){
$script = 'start /B "" '.$script;
}else{
$script = $script.' &';
}
$this->log('[command] '.$script."\n");
//@shell_exec($script);@shell_exec("powershell.exe Start-Process -WindowStyle hidden '".$command."' ");
proc_close(proc_open($script,array(array('pipe','r'),array('pipe','w'),array('pipe','w')),$pipes));
$this->progress($tempPath,$fileInfo,$cachePath,$timeStart);
return true;
}
/**
* 启动转换后计算进度, 并存储在全局任务中;
* 定时检测转换进度: 300ms一次, 结束条件: 转换完成or输出日志文件5s没有更新;
*/
private function progress($tempPath,$fileInfo,$cachePath,$timeStart){
$logFile = $tempPath.'.log';// 5s没有更新则结束;
usleep(300*1000);// 运行后,等待一段时间再读取信息;
$tempName = get_path_this($tempPath);
$pid = $this->processFind($tempName);
$data = $this->progressGet($logFile);
$args = array($tempPath,$fileInfo,$cachePath,$timeStart,$pid);
$task = new TaskConvert($fileInfo['taskID'],'videoResize',$data['total'],LNG("fileThumb.video.title"));
$task->task['currentTitle'] = size_format($fileInfo['size']).'; '.$fileInfo['name'];
$task->onKillCall = array(array($this,'convertFinished'),$args);
$task->onKillSet(array($this,'convertFinished'),$args);
$this->log('[start] '.$task->task['currentTitle'].'; pid='.$pid);
while(true){
$data = $this->progressGet($logFile);clearstatcache();
//进程已不存在; 转码报错或者意外终止或者其他情况的进程终止;
if($data['total'] && $data['finished'] == $data['total']){$this->log('stop-finished;');break;}
if(time() - @filemtime($logFile) >= 20){$this->log('stop-time;');break;}
if(!$this->processFind($tempName)){$this->log('stop-pid;');break;}
$task->task['taskFinished'] = round($data['finished'],3);
CacheLock::lock("video-process-update");
$task->update(0);
CacheLock::unlock("video-process-update");
$this->log(sprintf('%.1f',$data['finished'] * 100 / $data['total']).'%',true);
Cache::set($fileInfo['taskID'],'running',60);
usleep(300*1000);//300ms;
}
$task->end();$task->onKill();
}
public function log($log,$clear=false,$show=true){
if($show){echoLog($log,$clear);}
if(!$clear){write_log($log,'videoConvert');}
}
private function progressGet($logFile){
$result = array('finished'=>0,'total'=>0);
$content = @file_get_contents($logFile);
preg_match("/Duration: (.*?), start: (.*?), bitrate:/", $content, $match);
if(is_array($match)){
$total = explode(':', $match[1]);
$result['total'] = intval($total[0]) * 3600 + intval($total[1]) * 60 + floatval($total[2]); // 转换为秒
}
preg_match_all("/frame=(.*?) time=(.*?) bitrate/", $content, $match);
if(is_array($match) && count($match) == 3 && $match[2]){
$total = explode(':',$match[2][count($match[2]) - 1]);
$result['finished'] = intval($total[0]) * 3600 + intval($total[1]) * 60 + floatval($total[2]); // 转换为秒
}
if(!$result['total']){$result['total'] = 1;}
if(strstr($content,'video:')){$result['finished'] = $result['total'];}
return $result;
}
public function convertFinished($tempPath,$fileInfo,$cachePath,$timeStart,$pid){
$logFile = $tempPath.'.log';
$data = $this->progressGet($logFile);
$output = @file_get_contents($logFile);
@unlink($logFile);
$this->processKill($pid);
$this->convertClear($fileInfo['taskID']);
$runError = true;
$errorTips = 'Run error!';$cacheTime = 3600;
if( preg_match("/(Error .*)/",$output,$match) ||
preg_match("/(Unknown encoder .*)/",$output,$match) ||
preg_match("/(Invalid data found .*)/",$output,$match) ||
preg_match("/(No device available .*)/",$output,$match)
){
$errorTips = '[ffmpeg error] '.$match[0].';<br/>see log[data/temp/log/videoconvert/xx.log]';
}
if( preg_match("/frame=\s+(\d+)/",$output,$match)){
$errorTips = 'Stoped!';
$runError = false;
}
$logEnd = get_caller_msg();
$logTime = 'time='.(time() - $timeStart);
if( $data['total'] && intval($data['total']) == intval($data['finished']) &&
file_exists($tempPath) && is_file($tempPath) && filesize($tempPath) > 100 ){
$runError = true;
$destPath = IO::move($tempPath,$cachePath);
$checkStr = IO::fileSubstr($destPath,0,10);
if($checkStr && strlen($checkStr) == 10){
$this->log('[end] '.$fileInfo['name'].'; finished Success; '.$logTime.$logEnd);
return;
}
IO::remove($destPath,false);
$this->log('[end] '.$fileInfo['name'].'; move error; '.$logTime.$logEnd);
$errorTips = 'Move temp file error!';$cacheTime = 60;
}
@unlink($tempPath);
Cache::set($fileInfo['taskID'],'error',5);
$this->convertError($fileInfo['taskID'],$errorTips,$cacheTime);
$logAdd = $runError ? "\n".trim($output) : '';
$this->log('[end] '.$fileInfo['name'].';'.$errorTips.'; '.$logTime.$logAdd.$logEnd);
$this->log('[end] '.$output);
}
public function convertAdd($taskID){
$runTaskCount = intval(Cache::get('fileThumb-videoResizeCount'));
$taskList = Cache::get('fileThumb-videoResizeList');
$taskList = is_array($taskList) ? $taskList : array();
if(!$runTaskCount){$this->stopAll();$taskList = array();}
$taskList[$taskID] = '1';
Cache::set('fileThumb-videoResizeList',$taskList,3600);
Cache::set('fileThumb-videoResizeCount',($runTaskCount + 1),3600);
Cache::set($taskID,'running',600);
}
public function convertClear($taskID){
$runTaskCount = intval(Cache::get('fileThumb-videoResizeCount'));
$runTaskCount = $runTaskCount <= 1 ? 0 : ($runTaskCount - 1);
$taskList = Cache::get('fileThumb-videoResizeList');
$taskList = is_array($taskList) ? $taskList : array();
if($taskList[$taskID]){unset($taskList[$taskID]);}
Cache::set('fileThumb-videoResizeList',$taskList,3600);
Cache::set('fileThumb-videoResizeCount',$runTaskCount,3600);
Cache::remove($taskID);
if(!$runTaskCount){$this->stopAll();}
}
public function stopAll(){
$taskList = Cache::get('fileThumb-videoResizeList');
$taskList = is_array($taskList) ? $taskList : array();
foreach ($taskList as $taskID=>$key){
Task::kill($taskID);
Cache::remove($taskID);
$this->convertError($taskID,-1);
}
Cache::remove('fileThumb-videoResizeList');
Cache::remove('fileThumb-videoResizeCount');
$count = 0;
while($count <= 20){
$pid = $this->processFind('ffmpeg');
if($pid){$this->processKill($pid);}
if(!$pid){break;}
$count ++;
}
}
private function convertError($taskID,$content="",$cacheTime=0){
$key = 'fileThumb-videoResizeError-'.$taskID;
if($content === -1){return Cache::remove($key);}
if(!$content){return Cache::get($key);}
if(!$cacheTime){$cacheTime = 3600*24*3;}
Cache::set($key,$content,$cacheTime);
}
private function convertSupport($ffmpeg){
$out = shell_exec($ffmpeg.' -v 2>&1');
if(!strstr($out,'--enable-libx264')){return false;}
return true;
}
// 通过命令行及参数查找到进程pid; 兼容Linux,window,mac
// http://blog.kail.xyz/post/2018-03-28/other/windows-find-kill.html
public function processFind($search){
$this->setLctype($search);
$cmd = "ps -eo user,pid,ppid,args | grep '".escapeShell($search)."' | grep -v grep | awk '{print $2}'";
if($GLOBALS['config']['systemOS'] != 'windows'){return trim(@shell_exec($cmd));}
// windows 获取pid;
$cmd = 'WMIC process where "Commandline like \'%%%'.$search.'%%%\'" get Caption,Processid,Commandline';
$res = trim(@shell_exec($cmd));
$resArr = explode("\n",trim($res));
if(!$resArr || count($resArr) <= 3) return '';
$lineFind = $resArr[count($resArr) - 3];// 最后两个一个为wmic,和cmd;
$res = preg_match("/.*\s+(\d+)\s*$/",$lineFind,$match);
if($res && is_array($match) && $match[1]){return $match[1];}
return '';
}
// 通过pid结束进程;
public function processKill($pid){
if(!$pid) return;
if($GLOBALS['config']['systemOS'] != 'windows'){return @shell_exec('kill -9 '.$pid);}
@shell_exec('taskkill /F /PID '.$pid);
}
// ================================== 生成视频预览图 ==================================
// 生成视频预览图; 平均截取300张图(小于30s的视频不截取)
public function videoPreview($plugin){
$pickCount = 300;// 总共截取图片数(平均时间内截取)
$path = $plugin->filePath($GLOBALS['in']['path']);
$fileInfo = IO::info($path);
$fileInfo = Action('explorer.list')->pathInfoMore($fileInfo);
$tempFileName = 'preview-'.KodIO::hashPath($fileInfo).'.jpg';
$findSource = IO::fileNameExist($plugin->cachePath,$tempFileName);
if($findSource){return IO::fileOut(KodIO::make($findSource));}
$command = $plugin->getFFmpeg();
if(!$command){return show_json('command error',false);}
$localFile = $plugin->localFile($path);
if(!$localFile ){return show_json('not local file',false);}
$videoInfo = $this->parseVideoInfo($command,$localFile);
$this->storeVideoMetadata($fileInfo,$videoInfo);
if(!$videoInfo || $videoInfo['playtime'] <= 30){return show_json('time too short!',false);}
// $pickCount = $totalTime; 每秒生成一张图片; 可能很大
// 宽列数固定10幅图, 行数为总截取图除一行列数; https://blog.51cto.com/u_15639793/5297432
if(Cache::get($tempFileName)) return show_json('running');
Cache::set($tempFileName,'running',600);
ignore_timeout();
$tempPath = TEMP_FILES.$tempFileName;
$fps = $pickCount / $videoInfo['playtime'];
$sizeW = 150;
$tile = '10x'.ceil($pickCount / 10).'';
$scale = 'scale='.$sizeW.':-2'; //,pad='.$sizeW.':'.$sizeH.':-1:-1 -q:v 1~5 ;质量从最好到最差;
$args = '-sws_flags accurate_rnd -q:v 4 -an'; //更多参数; 设置图片缩放算法(不设置缩小时可能产生绿色条纹花屏);
$this->setLctype($localFile,$tempPath);
$cmd = $plugin->memLimitParam('ffmpeg', $command).' -y -i '.escapeShell($localFile).' -vf "fps='.$fps.','.$scale.',tile='.$tile.'" '.$args.' '.escapeShell($tempPath);
$this->log('[videoPreview start] '.$fileInfo['name'].';size='.size_format($fileInfo['size']),0,0);$timeStart = timeFloat();
$this->log('[videoPreview run] '.$cmd,0,0);
@shell_exec($cmd);//pr($cmd);exit;
Cache::remove($tempFileName);
$success = file_exists($tempPath) && filesize($tempPath) > 100;
$msg = $success ? 'success' : 'error';
$this->log('[videoPreview end] '.$fileInfo['name'].';time='.(timeFloat() - $timeStart).'s;'.$msg,0,0);
if($success){
$destPath = IO::move($tempPath,$plugin->cachePath);
if($destPath){return IO::fileOut($destPath);}
}
@unlink($tempPath);
show_json('run error',false);
}
// 更新完善文件Meta信息;
private function storeVideoMetadata($fileInfo,$videoInfo){
$infoMore = _get($fileInfo,'fileInfoMore',array());
$cacheKey = 'fileInfo.'.md5($fileInfo['path'].'@'.$fileInfo['size'].$fileInfo['modifyTime']);
$fileID = _get($fileInfo,'fileID');
$fileID = _get($fileInfo,'fileInfo.fileID',$fileID);
$audio = $videoInfo['audio'];
$infoMore['audio'] = is_array($infoMore['audio']) ? array_merge($audio,$infoMore['audio']) : $audio;
$infoMore = array_merge($videoInfo,$infoMore);
$infoMore['etag'] = $fileID ? $fileID : $cacheKey;
if($fileID){
Model("File")->metaSet($fileID,'fileInfoMore',json_encode($infoMore));
}else{
Cache::set($cacheKey,$infoMore,3600*24*30);
}
}
// 解析视频文件信息;
private function parseVideoInfo($command,$video){
$this->setLctype($video);
$result = shell_exec($command.' -i '.escapeShell($video).' 2>&1');
$info = array('playtime'=>0,'createTime'=>'','audio'=>array());
if(preg_match("/Duration:\s*([0-9\.\:]+),/", $result, $match)) {
$total = explode(':', $match[1]);
$info['playtime'] = intval($total[0]) * 3600 + intval($total[1]) * 60 + floatval($total[2]); // 转换为秒
}
if(preg_match("/Video: (.*?), (.*?), (\d+)x(\d+)/", $result,$match)) {
$info['dataformat'] = $match[1];
$info['sizeWidth'] = $match[3];
$info['sizeHeight'] = $match[4];
}
if(preg_match("/Audio: (.*?), (\d+) Hz, (.*?), /", $result,$match)) {
$info['audio']['dataformat'] = $match[1];
$info['audio']['rate'] = $match[2];
$info['audio']['channelmode'] = $match[3];
}
if(preg_match("/creation_time\s*:\s*(.*)/", $result,$match)){
$info['createTime'] = date("Y-m-d H:i:s",strtotime($match[1]));
}
if(preg_match("/encoder\s*:\s*(.*)/", $result,$match)){
$info['software'] = $match[1];
}
return $info;
}
private function findBinPath($bin,$check){
$isWin = $GLOBALS['config']['systemOS'] == 'windows';
$path = false;
if ($isWin) {
exec('where '.escapeshellarg($bin),$output);
foreach ($output as $line) {
if (stripos($line,$check) !== false) {
$path = escapeshellarg($line).'/'.$bin.'.exe';
break;
}
}
} else {
exec('which '.$bin,$output,$status);
if ($status == 0) {
$path = $bin;
if($output[0]){
$path = $output[0].'/'.$bin;
}
}
}
return $path;
}
// 设置地区字符集,避免中文被过滤
private function setLctype($path,$path2=''){
if (stripos(setlocale(LC_CTYPE, 0), 'utf-8') !== false) return;
if (Input::check($path,'hasChinese') || ($path2 && Input::check($path2,'hasChinese'))) {
setlocale(LC_CTYPE, "en_US.UTF-8");
}
}
}