<?php
/**
* SeekQuarry/Yioop --
* Open Source Pure PHP Search Engine, Crawler, and Indexer
*
* Copyright (C) 2009 - 2026 Chris Pollett chris@pollett.org
*
* LICENSE:
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* END LICENSE
*
* @author Chris Pollett chris@pollett.org (initial MediaJob class
* and subclasses based on work of Pooja Mishra for her master's)
* @license https://www.gnu.org/licenses/ GPL3
* @link https://www.seekquarry.com/
* @copyright 2009 - 2026
* @filesource
*/
namespace seekquarry\yioop\library\media_jobs;
use seekquarry\yioop\configs as C;
use seekquarry\yioop\library as L;
use seekquarry\yioop\library\UrlParser;
/**
* Media Job used to convert videos uploaded to the wiki or group feeds to
* a common format (mp4)
*/
class VideoConvertJob extends MediaJob
{
/**
* Supported file types of videos that we can convert to mp4.
* @var array
*/
public $video_convert_types = ["mov", "avi", "m2ts"];
/**
* Datasource used to do directory level file manipulations (delete or
* traverse)
* @var object
*/
public $db;
/**
* Sets up the datasource used for the video convert directories
*/
public function init()
{
$db_class = C\NS_DATASOURCES . ucfirst(C\p('DBMS')). "Manager";
$this->db = new $db_class();
$this->db->connect();
}
/**
* Only run the VideoConvertJob if in distributed mode
*
* @return bool true if the job should run on this media-updater
* instance (i.e. the media updater is running in distributed mode)
*/
public function checkPrerequisites()
{
return $this->media_updater->media_mode == 'distributed';
}
/**
* Check for videos to convert. If found split to a common size to
* send to client media updaters. (Run on name server)
*/
public function prepareTasks()
{
$this->splitVideos();
}
/**
* Checks if video convert task is complete for a video. If so, moves
* movie segments to a converted folder, assembles the segments into
* a single video file, and moves the result to the desired place.
*/
public function finishTasks()
{
$this->moveVideoFoldersToConvertedDirectory();
$this->generateAssembleVideoFile();
$this->concatenateVideos();
}
/**
* Called from run() with conversion tasks from name server. If there are
* any mov or avi segmentconverts them mp4
* This function would only be called by client media updaters.
*
* @param array $tasks list of per-folder convert tasks dispatched by
* the name server; each task identifies a video segment folder this
* client should convert
* @return mixed the upload-task descriptor on a successful upload
* back to the name server, false on failure, or void if the convert
* folder couldn't be created
*/
public function doTasks($tasks)
{
$convert_folder = C\SCHEDULES_DIR . self::CONVERT_FOLDER;
if (!file_exists($convert_folder)) {
set_error_handler(null);
@mkdir($convert_folder);
restore_error_handler();
if (!file_exists($convert_folder)) {
L\crawlLog("----Unable to create $convert_folder. Bailing!");
return;
}
}
$db = $this->db;
$folders = glob($convert_folder . "/*", GLOB_ONLYDIR);
if (count($folders) > 0) {
foreach ($folders as $folder) {
$db->unlinkRecursive($folder);
}
}
if (!empty($tasks['data']) && !empty($tasks['file_name']) &&
!empty($tasks['folder_name'])) {
$data = $tasks['data'];
$folder_name = $tasks['folder_name'];
$file_name = $tasks['file_name'];
$convert_path = $convert_folder . "/" . $folder_name;
if (file_exists( $convert_path)) {
$db->unlinkRecursive( $convert_path);
}
mkdir($convert_path);
$downloaded_file = $convert_path . "/" . $file_name;
file_put_contents($downloaded_file, $data);
$this->convertVideo($downloaded_file);
$files = glob($convert_path . "/*.{mp4}", GLOB_BRACE);
if (!$files[0]) {
L\crawlLog("Will try to convert the file again later");
} else {
$converted_file_name = substr($files[0],
strlen($convert_path) + 1);
/* Upload the file to the server */
$file_data = file_get_contents($files[0]);
$upload_task['data'] = $file_data;
$upload_task['file_name'] = $converted_file_name;
$upload_task['folder_name'] = $folder_name;
L\crawlLog("Bundling upload data");
return $upload_task;
}
} else {
L\crawlLog("No files on server to convert!");
return false;
}
}
/**
* Generates a thumbnail from a video file assuming FFMPEG
*
* @param string $video_name full name and path of video file to make
* thumbnail from
* @param string $thumb_name full name and path for thumbnail file
*/
public function thumbFileFromVideo($video_name, $thumb_name)
{
$make_thumb_string =
C\FFMPEG." -i \"$video_name\" -vframes 1 -map 0:v:0".
" -vf \"scale=" . C\THUMB_DIM . ":" . C\THUMB_DIM."\" ".
"\"$thumb_name\" 2>&1";
L\crawlLog("----Making thumb with $make_thumb_string");
exec($make_thumb_string);
clearstatcache($thumb_name);
}
/**
* Splits a video into small chunks of 5 minutes
*
* @param string $file_path full path of video file to be split
* @param string $file_name name of video file along with extension
* @param string $destination_directory destination directory name
* where split files would be produced
*/
public function splitVideo($file_path, $file_name, $destination_directory)
{
L\crawlLog("----Splitting $file_path/$file_name...");
$extension = "." . UrlParser::getDocumentType($file_name, "");
$new_name = substr($file_name, 0, -strlen($extension));
$ffmpeg = C\FFMPEG." -i \"$file_path/$file_name\" ".
" -acodec copy -f segment -segment_time 150 ".
"-vcodec copy -reset_timestamps 1 -map 0 ".
"\"$destination_directory/%d$new_name$extension\"";
L\crawlLog($ffmpeg);
exec($ffmpeg);
}
/**
* Function to look through all the video directories present in media.
* convert folder generated by group model and split the eligible files.
*/
public function splitVideos()
{
$convert_folder = C\SCHEDULES_DIR . self::CONVERT_FOLDER;
if (!C\nsdefined('FFMPEG') || !file_exists($convert_folder)) {
return;
}
L\crawlLog("----Looking for video files to split...");
$type_string = "{" . implode(",", $this->video_convert_types) . "}";
$video_paths = glob($convert_folder."/*");
foreach ($video_paths as $video_path) {
if (is_dir($video_path)){
if (!file_exists($video_path . self::SPLIT_FILE)) {
return;
}
if (file_exists($video_path . self::SPLIT_FILE)) {
L\crawlLog("----Splitting the video $video_path");
$lines = file($video_path.self::FILE_INFO);
$folder_name = rtrim($lines[1]);
$file_name = rtrim($lines[3]);
L\crawlLog("----$folder_name : $file_name");
if ($folder_name && $file_name){
$this->splitVideo($folder_name, $file_name,
$video_path);
unlink($video_path . self::SPLIT_FILE);
file_put_contents($video_path . self::COUNT_FILE,
count(glob($video_path . "/*.$type_string",
GLOB_BRACE)));
}
}
}
}
}
/**
* Function to look through all the video directories present in media.
* convert folder and move them to converted folders if all the split files.
* are converted and are present in video directory under converted.
*/
public function moveVideoFoldersToConvertedDirectory()
{
L\crawlLog("----Moving video folders from media_convert to ".
"converted...");
$convert_folder = C\SCHEDULES_DIR . self::CONVERT_FOLDER;
$converted_folder = C\SCHEDULES_DIR . self::CONVERTED_FOLDER;
if (!file_exists($converted_folder)) {
mkdir($converted_folder);
}
$video_paths = glob($convert_folder . "/*");
foreach ($video_paths as $video_path) {
L\crawlLog("----Video Path : $video_path");
$actual_count = file_get_contents($video_path . self::COUNT_FILE);
L\crawlLog("----Actual_count : $actual_count");
$timestamp_files = glob($video_path."/*.time.txt");
$checked_out = count($timestamp_files);
L\crawlLog(" ----Checked out count : $checked_out");
$video_folder = str_replace($convert_folder."/", "", $video_path);
$converted_video_path = $converted_folder . "/" . $video_folder;
$converted_count = count(glob($converted_video_path .
"/*.{mp4}", GLOB_BRACE));
L\crawlLog("----Converted count : $converted_count");
if ($converted_count == $actual_count) {
L\crawlLog("----Conversion of segments complete!");
rename($video_path . self::COUNT_FILE,
$converted_video_path . self::COUNT_FILE);
rename($video_path . self::FILE_INFO,
$converted_video_path . self::FILE_INFO);
$this->db->unlinkRecursive($video_path);
}
}
}
/**
* Function to look through all the converted video directories present in
* media and generate the assemble video files needed for concatenating the
* converted splitfiles.
*/
public function generateAssembleVideoFile()
{
L\crawlLog("----Inside generateAssembleVideoFile function...");
$converted_folder = C\WORK_DIRECTORY . self::CONVERTED_FOLDER;
if (!file_exists($converted_folder)) {
mkdir($converted_folder);
}
foreach (glob($converted_folder."/*") as $video_path) {
if (file_exists($video_path . self::ASSEMBLE_FILE)) {
continue;
}
if (!file_exists($video_path.self::COUNT_FILE)) {
continue;
}
$actual_count = file_get_contents($video_path . self::COUNT_FILE);
$video_segments = glob($video_path . "/*.mp4");
$converted_count = count($video_segments);
if ($actual_count == $converted_count) {
foreach ($video_segments as $video_segment){
file_put_contents($video_path . self::ASSEMBLE_FILE,
"file "."'".(str_replace($video_path."/", "",
$video_segment))."'", FILE_APPEND);
file_put_contents($video_path.self::ASSEMBLE_FILE,
PHP_EOL, FILE_APPEND);
}
}
}
}
/**
* Concatenates split video files to generate one video file
*
* @param string $text_file_name file path containing.the relative file.
* paths of the files to be concatenated
* @param string $file_name name of video file to be given to output file.
* @param string $destination_directory destination directory name
* where concatenated file would be produced
* @return bool true if the concatenated output file was produced,
* false otherwise
*/
public function mergeVideo($text_file_name, $file_name,
$destination_directory)
{
$extension = "." . UrlParser::getDocumentType($file_name, "");
$new_name = substr($file_name, 0, -strlen($extension));
if (!file_exists($text_file_name)) {return; }
$generate_output = $destination_directory."/$new_name.mp4";
$ffmpeg = C\FFMPEG." -f concat -i \"$text_file_name\" -c copy ".
"\"$generate_output\"";
L\crawlLog($ffmpeg);
exec($ffmpeg);
if (file_exists($generate_output)) {
return true;
}
return false;
}
/**
* Function to look through each video directory and call the function to
* concatenate split files.
*/
public function concatenateVideos()
{
L\crawlLog("--Concatenating videos...");
$converted_folder = C\WORK_DIRECTORY . self::CONVERTED_FOLDER;
if (!file_exists($converted_folder)) {
mkdir($converted_folder);
}
foreach (glob($converted_folder."/*") as $video_path) {
L\crawlLog("----Video Path " . $video_path);
if (is_dir($video_path)){
if (!file_exists($video_path . self::ASSEMBLE_FILE)) {
continue;
}
$assemble_file = $video_path . self::ASSEMBLE_FILE;
$lines = file($video_path . self::FILE_INFO);
$folder = trim($lines[1]);
$thumb_folder = trim($lines[2]);
$file_name = trim($lines[3]);
if ($this->mergeVideo($assemble_file, $file_name, $folder)){
$this->db->unlinkRecursive($video_path);
$video_name = $folder. "/" . $file_name;
$extension_len = strlen(
UrlParser::getDocumentType($video_name));
$file_prefix = substr($file_name, 0, -$extension_len - 1);
$thumb_file_name = $file_prefix . ".mp4.jpg";
$thumb_name = $thumb_folder . "/" . $thumb_file_name;
$this->thumbFileFromVideo($video_name, $thumb_name);
}
}
}
}
/**
* Function to convert avi or mov file to mp4 format.
*
* @param string $file_name full path of the file.
*/
public function convertVideo($file_name)
{
$extension = "." . UrlParser::getDocumentType($file_name, "");
$new_name = substr($file_name, 0, -strlen($extension));
switch ($extension)
{
case '.mov':
$ffmpeg = C\FFMPEG." -i \"$file_name\" ".
" -vcodec h264 -acodec aac -preset veryfast -crf 28 ".
"-strict -2 \"$new_name.mp4\"";
break;
case '.avi':
$ffmpeg = C\FFMPEG." -i \"$file_name\" ".
" -vcodec libx264 -preset slow -acodec aac -crf 28 ".
"-strict experimental -b:a 192k -ac 2 \"$new_name.mp4\"";
break;
}
L\crawlLog($ffmpeg);
exec($ffmpeg);
}
/**
* Handles request to upload the posted data (video file data)
* in correct location as per the request attributes such as
* folder name and file name.
* @param int $machine_id id of machine doing the put
* @param array $data associative array about converted segment
* @return string message concerning success or non-success of upload
*/
public function putTasks($machine_id, $data)
{
if (!isset($data['data']) || !isset($data['folder_name']) ||
!isset($data['file_name'])) {
return "Missing parameters in upload message";
}
$convert_folder = C\SCHEDULES_DIR . self::CONVERT_FOLDER;
$converted_folder = C\SCHEDULES_DIR . self::CONVERTED_FOLDER;
$file = $data['data'];
$folder_name = $data['folder_name'];
$file_name = $data['file_name'];
$upload_path = $converted_folder . "/" . $folder_name . "/" .
$file_name;
$original_split_pre_file = $convert_folder . "/" . $folder_name."/".
substr($file_name, 0, -4);
if (!$data) {
return "No data received by web server.";
}
$upload_flag = false;
if (file_exists($converted_folder . "/" . $folder_name)){
if (file_exists($upload_path)){
return "Video file had already been uploaded!";
} else {
file_put_contents($upload_path, $file);
$upload_flag = true;
}
} else {
if (!file_exists($converted_folder)) {
mkdir($converted_folder);
}
mkdir($converted_folder . "/" . $folder_name);
file_put_contents($upload_path, $data);
$upload_flag = true;
}
$out = "Deleting pre-convert-segment:\n$original_split_pre_file\n";
if ($upload_flag) {
$originals =
glob($original_split_pre_file.".{mov,avi}", GLOB_BRACE);
foreach ($originals as $original) {
unlink($original);
}
}
return $out . "Upload success!";
}
/**
* Handles the request to get the video file from the video directory for
* conversion. This selection is based upon if the file was taken
* previously or not. If it was then its timestamp is checked.
* Otherwise new file is sent for conversion along with its folder name.
*
* @param int $machine_id not used
* @param array $data not used
* @return array an associate array with info on a file to convert
*/
public function getTasks($machine_id, $data = null)
{
$convert_folder = C\SCHEDULES_DIR . self::CONVERT_FOLDER;
$current_time = time();
$file_path = false;
foreach (glob($convert_folder . "/*") as $folder) {
foreach (glob($folder."/*.{mov,avi}", GLOB_BRACE) as $file) {
$folder_name = str_replace("$convert_folder/", "", $folder);
$file_name = str_replace($folder."/", "", $file);
$time_file_name = $folder . "/" . $file_name . ".time.txt";
if (file_exists($time_file_name)) {
$file_time = file_get_contents($time_file_name);
if ($current_time - $file_time >
C\MAX_FILE_TIMESTAMP_LIMIT) {
file_put_contents($time_file_name, $current_time);
$file_path = C\SCHEDULES_DIR . self::CONVERT_FOLDER .
"/$folder_name/$file_name";
}
} else {
file_put_contents($time_file_name, $current_time);
$file_path = C\SCHEDULES_DIR . self::CONVERT_FOLDER .
"/$folder_name/$file_name";
}
if ($file_path) {
break 2;
}
}
}
if ($file_path) {
$convert_task = [];
$convert_task['data'] = file_get_contents($file_path);
$convert_task['file_name'] = $file_name;
$convert_task['folder_name'] = $folder_name;
return $convert_task;
}
return false;
}
}