diff --git a/3.0/modules/transcode/controllers/admin_transcode.php b/3.0/modules/transcode/controllers/admin_transcode.php new file mode 100644 index 00000000..b91b1284 --- /dev/null +++ b/3.0/modules/transcode/controllers/admin_transcode.php @@ -0,0 +1,166 @@ + 0) { + module::set_var("transcode", "ffmpeg_path", $_REQUEST['ffmpeg_path']); + $data['success'] = true; + $data['codecs'] = self::_get_supported_audio_codecs($_REQUEST['ffmpeg_path']); + } + else { + $error = ""; + switch ($val) { + case 0: $error = "Empty file path provided"; break; + case -1: $error = "File does not exist"; break; + case -2: $error = "Path is a directory"; break; + default: $error = "Unspecified error"; + } + $data['error'] = $error; + } + echo json_encode($data); + } + + public function index() { + $form = $this->_get_form(); + + if (request::method() == "post") { + access::verify_csrf(); + + if ($form->validate()) { + module::set_var("transcode", "ffmpeg_path", $_POST['ffmpeg_path']); + module::set_var("transcode", "ffmpeg_flags", $_POST['ffmpeg_flags']); + module::set_var("transcode", "audio_codec", $_POST['audio_codec']); + module::set_var("transcode", "ffmpeg_audio_kbits", (isset($_POST['ffmpeg_audio_kbits']) ? $_POST['ffmpeg_audio_kbits'] : false)); + + module::set_var("transcode", "resolution_240p", (isset($_POST['resolution_240p']) ? $_POST['resolution_240p'] : false)); + module::set_var("transcode", "resolution_360p", (isset($_POST['resolution_360p']) ? $_POST['resolution_360p'] : false)); + module::set_var("transcode", "resolution_480p", (isset($_POST['resolution_480p']) ? $_POST['resolution_480p'] : false)); + module::set_var("transcode", "resolution_576p", (isset($_POST['resolution_576p']) ? $_POST['resolution_576p'] : false)); + module::set_var("transcode", "resolution_720p", (isset($_POST['resolution_720p']) ? $_POST['resolution_720p'] : false)); + module::set_var("transcode", "resolution_1080p", (isset($_POST['resolution_1080p']) ? $_POST['resolution_1080p'] : false)); + + message::success(t("Settings have been saved")); + url::redirect("admin/transcode"); + } + else { + message::error(t("There was a problem with the submitted form. Please check your values and try again.")); + } + } + + print $this->_get_view(); + } + + private function _get_view($form = null) { + $v = new Admin_View("admin.html"); + $v->page_title = t("Gallery 3 :: Manage Transcoding Settings"); + + $v->content = new View("admin_transcode.html"); + $v->content->form = empty($form) ? $this->_get_form() : $form; + + return $v; + } + + private function _get_supported_audio_codecs($given_path = null) { + + $flv_compatible_codecs = array("aac" => "aac", "adpcm_swf" => "adpcm_swf", "mp3" => "libmp3lame"); + + if ($given_path) + $ffmpegPath = $given_path; + else + $ffmpegPath = module::get_var("transcode", "ffmpeg_path", transcode::whereis("ffmpeg")); + + $legacy = false; + exec($ffmpegPath . " -codecs", $codecs); + if (count($codecs) == 0) { + $legacy = true; + exec($ffmpegPath . " -formats 2>&1", $codecs); + } + + $search = true; + if ($legacy) $search = false; + + $found = array(); + foreach ($codecs as $line) { + if ($search) { + if (strpos($line, "DEA")) { + $bits = preg_split("/[\s]+/", $line); + if (in_array($bits[2], $flv_compatible_codecs)) { + $key = array_search($bits[2], $flv_compatible_codecs); + $found[$key] = $flv_compatible_codecs[$key]; + } + } + } + + if ($legacy && strpos($line, "Codecs:") !== false) { + $search = true; + continue; + } + if ($legacy && $search && strpos($line, "Bitstream filters:")) { + $search = false; + continue; + } + } + return $found; + } + + private function _get_form() { + $form = new Forge("admin/transcode", "", "post", array("id" => "g-admin-transcode-form")); + + $group = $form->group("system")->label(t("System")); + + $ffmpegPath = transcode::whereis("ffmpeg"); + $codecs = $this->_get_supported_audio_codecs(); + + $group ->input("ffmpeg_path") + ->id("ffmpeg_path") + ->label(t("Path to ffmpeg binary:")) + ->value(module::get_var("transcode", "ffmpeg_path", $ffmpegPath)) + ->callback("transcode::verify_ffmpeg_path") + ->error_messages("required", t("You must enter the path to ffmpeg")) + ->error_messages("invalid", t("File does not exist")) + ->error_messages("is_dir", t("File is a directory")) + ->message("Auto detected ffmpeg here: " . $ffmpegPath . "
Click here to verify ffmpeg path and continue."); + + $group ->input("ffmpeg_flags") + ->id("ffmpeg_flags") + ->label(t("Extra ffmpeg flags:")) + ->value(module::get_var("transcode", "ffmpeg_flags")); + + $group ->dropdown("audio_codec") + ->id("audio_codec") + ->label(t("Audio codec to use:")) + ->options($codecs) + ->selected(module::get_var("transcode", "audio_codec")); + + $group ->checkbox("ffmpeg_audio_kbits") + ->label(t("Send audio bitrate as kbits instead of bits/s")) + ->checked(module::get_var("transcode", "ffmpeg_audio_kbits")); + + $group = $form->group("resolutions")->label(t("Resolutions")); + + $group ->checkbox("resolution_240p") + ->label("240p") + ->checked(module::get_var("transcode", "resolution_240p")); + $group ->checkbox("resolution_360p") + ->label("360p") + ->checked(module::get_var("transcode", "resolution_360p")); + $group ->checkbox("resolution_480p") + ->label("480p") + ->checked(module::get_var("transcode", "resolution_480p")); + $group ->checkbox("resolution_576p") + ->label("576p") + ->checked(module::get_var("transcode", "resolution_576p")); + $group ->checkbox("resolution_720p") + ->label("720p") + ->checked(module::get_var("transcode", "resolution_720p")); + $group ->checkbox("resolution_1080p") + ->label("1080p") + ->checked(module::get_var("transcode", "resolution_1080p")); + + $form->submit("submit")->value(t("Save")); + return $form; + } +} diff --git a/3.0/modules/transcode/debug.php b/3.0/modules/transcode/debug.php new file mode 100644 index 00000000..7e9d31a9 --- /dev/null +++ b/3.0/modules/transcode/debug.php @@ -0,0 +1,50 @@ +ffmpeg info"; + $ffmpegPath = whereis("ffmpeg"); + echo "path: " . $ffmpegPath . "
"; + $version = @shell_exec($ffmpegPath . " -version"); $version = explode("\n", $version); $version = $version[0]; $version = explode(" ", $version); $version = $version[1]; + echo "version: " . $version . "
"; + echo "codecs:
" . @shell_exec($ffmpegPath . " -codecs 2>&1") . "

"; + echo "formats:
" . @shell_exec($ffmpegPath . " -formats") . "

"; +} + +function whereis($app) { + $op = @shell_exec("whereis " . $app); + if ($op != "") { + $op = explode(" ", $op); + for ($i = 1; $i < count($op); $i++) { + if (file_exists($op[$i]) && !is_dir($op[$i])) + return $op[$i]; + } + } + return false; +} + +/* +stdClass Object +( + [video] => stdClass Object + ( + [codec] => h264, + [height] => 576 + [width] => 640 + ) + + [audio] => stdClass Object + ( + ) + +) + + */ \ No newline at end of file diff --git a/3.0/modules/transcode/helpers/transcode.php b/3.0/modules/transcode/helpers/transcode.php new file mode 100644 index 00000000..f6026ec2 --- /dev/null +++ b/3.0/modules/transcode/helpers/transcode.php @@ -0,0 +1,59 @@ +value); + switch ($v) { + case 0: $field->add_error("required", 1); break; + case -1: $field->add_error("invalid", 1); break; + case -2: $field->add_error("is_dir", 1); break; + } + } + + static function log($item) { + + if (is_string($item) || is_numeric($item)) {} + else + $item = print_r($item, true); + + $fh = fopen(VARPATH . "modules/transcode/log/transcode.log", "a"); + fwrite($fh, date("Y-m-d H:i:s") . ": " . $item . "\n"); + fclose($fh); + + } + +} diff --git a/3.0/modules/transcode/helpers/transcode_event.php b/3.0/modules/transcode/helpers/transcode_event.php new file mode 100644 index 00000000..774f1275 --- /dev/null +++ b/3.0/modules/transcode/helpers/transcode_event.php @@ -0,0 +1,204 @@ +get("settings_menu") + ->append(Menu::factory("link") + ->id("transcode_menu") + ->label(t("Video Transcoding")) + ->url(url::site("admin/transcode"))); + } + + static function item_deleted($item) { + if ($item->is_movie()) { + transcode::log("Deleting transcoded files for item " . $item->id); + if (is_dir(VARPATH . "modules/transcode/flv/" . $item->id)) { + self::rrmdir(VARPATH . "modules/transcode/flv/" . $item->id); + } + db::build()->delete("transcode_resolutions")->where("item_id", "=", $item->id)->execute(); + } + } + + static function rrmdir($dir) { + if (is_dir($dir)) { + $objects = scandir($dir); + foreach ($objects as $object) { + if ($object != "." && $object != "..") { + if (filetype($dir."/".$object) == "dir") + self::rrmdir($dir."/".$object); + else + unlink($dir."/".$object); + } + } + reset($objects); + rmdir($dir); + } + } + + private static function _getVideoInfo($file) { + $ffmpegPath = module::get_var("transcode", "ffmpeg_path"); + + $op = array(); + @exec($ffmpegPath . ' -i "' . $file . '" 2>&1', $op); + + $file = new stdclass(); + $file->video = new stdclass(); + $file->audio = new stdclass(); + $file->audio->has = false; + + foreach ($op as $line) { + transcode::log($line); + + if (preg_match('/Duration\: (\d{2}):(\d{2}):(\d{2})\.(\d{2})/', $line, $matches)) { + $file->video->duration = $matches[3] . "." . $matches[4]; + $file->video->duration += $matches[2] * 60; + $file->video->duration += $matches[1] * 3600; + } + else if (preg_match('/Stream #0\.\d(\(\w*\))?\: Video\:/', $line)) { + $bits = preg_split('/[\s]+/', $line); + + for ($i = 0; $i < count($bits); $i++) { + if ($bits[$i] == "fps,") $file->video->fps = $bits[$i - 1]; + else if ($bits[$i] == "kb/s,") $file->video->bitrate = $bits[$i - 1] * 1024; + } + + $file->video->codec = $bits[4]; + list($file->video->width, $file->video->height) = explode('x', $bits[6]); + } + else if (preg_match('/Stream #0\.\d(\(\w*\))?+\: Audio\: (\w*)\,/', $line, $matches)) { + $file->audio->has = true; + $file->audio->codec = $matches[2]; + + if (preg_match('/(\d*) Hz/', $line, $hz)) + $file->audio->samplerate = $hz[1]; + if (preg_match('/(\d*) channels?/', $line, $channels)) + $file->audio->channels = $channels[1]; + if (preg_match('/(\d*) kb\/s/', $line, $bitrate)) + $file->audio->bitrate = $bitrate[1]; + } + } + + return $file; + } + + static function item_created($item) { + if ($item->is_movie()) { + transcode::log("Item created - is a movie. Let's create a transcode task or 2.."); + $ffmpegPath = module::get_var("transcode", "ffmpeg_path"); + + $fileObj = self::_getVideoInfo($item->file_path()); + transcode::log($fileObj); + + // Save our needed variables + $srcFile = $item->file_path(); + $srcWidth = transcode::makeMultipleTwo($fileObj->video->width); + $srcHeight = transcode::makeMultipleTwo($fileObj->video->height); + $aspect = $srcWidth / $srcHeight; + + $srcFPS = $fileObj->video->fps; + + $srcAR = $fileObj->audio->samplerate; + if ($srcAR > 44100) $srcAR = 44100; + + $accepted_sample_rates = array(11025, 22050, 44100); + + if (!in_array($srcAR, $accepted_sample_rates)) { + // if the input sample rate isn't an accepted rate, find the next lowest rate that is + $below = true; $rate = 0; + if ($srcAR < 11025) { + $rate = 11025; + } + else { + foreach ($accepted_sample_rates as $r) { + transcode::log("testing audio rate '" . $r . "' against input rate '" . $srcAR . "'"); + if ($r < $srcAR) { + $rate = $r; + } + } + } + $srcAR = $rate; + } + + $srcACodec = module::get_var("transcode", "audio_codec"); + + $heights = array(); + if (module::get_var("transcode", "resolution_240p")) array_push($heights, 240); + if (module::get_var("transcode", "resolution_360p")) array_push($heights, 360); + if (module::get_var("transcode", "resolution_480p")) array_push($heights, 480); + if (module::get_var("transcode", "resolution_576p")) array_push($heights, 576); + if (module::get_var("transcode", "resolution_720p")) array_push($heights, 720); + if (module::get_var("transcode", "resolution_1080p")) array_push($heights, 1080); + + if (!is_dir(VARPATH . "modules/transcode/flv/" . $item->id)) + @mkdir(VARPATH . "modules/transcode/flv/" . $item->id); + + $xtraFlags = module::get_var("transcode", "ffmpeg_flags", ""); + + foreach ($heights as $destHeight) { + transcode::log("srcHeight: " . $srcHeight . ", destheight: " . $destHeight); + + // don't bother upscaling, there's no advantage to it... + if ($destHeight > $srcHeight) continue; + + $destFormat = module::get_var("transcode", "format", "flv"); + $destWidth = floor($destHeight * $aspect); + if ($destWidth % 2) + $destWidth = ceil($destHeight * $aspect); + + transcode::log("destination resolution: " . $destWidth . "x" . $destHeight); + + $destFile = VARPATH . "modules/transcode/flv/" . $item->id . "/" . $destWidth . "x" . $destHeight . ".flv"; + + switch ($destHeight) { + case 240: $destVB = 128; $srcAB = 16 * ($fileObj->audio->channels ? $fileObj->audio->channels : 2); break; + case 360: $destVB = 384; $srcAB = 16 * ($fileObj->audio->channels ? $fileObj->audio->channels : 2); break; + case 480: $destVB = 1024; $srcAB = 32 * ($fileObj->audio->channels ? $fileObj->audio->channels : 2); break; + case 576: $destVB = 2048; $srcAB = 32 * ($fileObj->audio->channels ? $fileObj->audio->channels : 2); break; + case 720: $destVB = 4096; $srcAB = 64 * ($fileObj->audio->channels ? $fileObj->audio->channels : 2); break; + case 1080; $destVB = 8192; $srcAB = 64 * ($fileObj->audio->channels ? $fileObj->audio->channels : 2); break; + } + $destVB *= 1024; + $srcAB *= 1024; + + $cmd = + $ffmpegPath . " " . + "-i \"" . $srcFile . "\" "; + if ($fileObj->audio->has) + $cmd .= + "-y -acodec " . $srcACodec . " " . + "-ar " . $srcAR . " " . + "-ab " . $srcAB . " "; + else + $cmd .= + "-an "; + + $cmd .= + "-vb " . $destVB . " " . + "-f " . $destFormat . " " . + "-s " . $destWidth . "x" . $destHeight . " " . + $xtraFlags . " " . + "\"" . $destFile . "\""; + + transcode::log($cmd); + + $task_def = + Task_Definition::factory() + ->callback("transcode_task::transcode") + ->name("Video Transcode to " . $destWidth . "x" . $destHeight) + ->severity(log::SUCCESS); + + $task = + task::create($task_def, + array("ffmpeg_cmd" => $cmd, + "width" => $destWidth, + "height" => $destHeight, + "item_id" => $item->id) + ); + + task::run($task->id); + } + } + } +} diff --git a/3.0/modules/transcode/helpers/transcode_installer.php b/3.0/modules/transcode/helpers/transcode_installer.php new file mode 100644 index 00000000..6501e4f0 --- /dev/null +++ b/3.0/modules/transcode/helpers/transcode_installer.php @@ -0,0 +1,42 @@ +query("CREATE TABLE IF NOT EXISTS {transcode_resolutions} ( + `id` int(9) NOT NULL auto_increment, + `item_id` int(9) NOT NULL, + `resolution` varchar(16) NOT NULL, + PRIMARY KEY (`id`) + ) DEFAULT CHARSET=utf8;"); + + @mkdir(VARPATH . "modules/transcode"); + @mkdir(VARPATH . "modules/transcode/log"); + @mkdir(VARPATH . "modules/transcode/flv"); + + self::setversion(); + } + static function uninstall() { + Database::instance()->query("DROP TABLE {transcode_resolutions}"); + dir::unlink(VARPATH . "modules/transcode"); + } + + static function upgrade($version) { + if ($version < self::getversion()) + self::setversion(); + } + + static function deactivate() {} + static function activate() {} + static function can_activate() { + $messages = array(); + if (!function_exists("exec")) { + $messages["warn"][] = t("exec() is required to auto-detect the ffmpeg binary. You must specify the path to the ffmpeg binary manually before you can convert videos."); + } + return $messages; + } +} diff --git a/3.0/modules/transcode/helpers/transcode_task.php b/3.0/modules/transcode/helpers/transcode_task.php new file mode 100644 index 00000000..da51bc27 --- /dev/null +++ b/3.0/modules/transcode/helpers/transcode_task.php @@ -0,0 +1,109 @@ +id . " started."); + + $cmd = $task->get("ffmpeg_cmd"); + + $logfile = VARPATH . "modules/transcode/log/" . time() . ".log"; + $task->set("logfile", $logfile); + + $output = ""; + $return_val = 0; + self::fork($cmd, $logfile); + + // wait for ffmpeg to fire up.. + sleep(2); + + while ($time = self::GetEncodedTime($logfile)) { + transcode::log("encoded time: " . $time); + + if (self::$duration > 0) { + $task->state = "encoding"; + $task->status = "Encoding..."; + $pct = sprintf("%0.0f", ($time / self::$duration) * 100); + $task->percent_complete = $pct; + $task->save(); + } + usleep(500000); + } + + $output = @file_get_contents($logfile); + + transcode::log("ffmpeg job completed."); + + if ($output) { + $task->percent_complete = 100; + $task->done = true; + $task->state = "success"; + $task->status = "Transcoding complete."; + + transcode::log("insert into transcode table to indicate success"); + $res = ORM::factory('transcode_resolution'); + $res->resolution = $task->get("width") . "x" . $task->get("height"); + $res->item_id = $task->get("item_id"); + $res->save(); + } + else { + transcode::log("Error transcoding. ffmpeg output:"); + transcode::log($output); + $task->percent_complete = 100; + $task->done = true; + $task->state = "error"; + $task->status = "Transcoding error."; + } + $task->save(); + + } + + static function fork($shellCmd, $logfile) { + $cmd = "nice " . $shellCmd . " > " . $logfile . " 2>&1 &"; + transcode::log("executing: " . $cmd); + exec($cmd); + } + + static function GetEncodedTime($logfile) { + if (!file_exists($logfile)) { + transcode::log("can't open FFMPEG-Log '" . $logfile . "'"); + return false; + } + else { + $FFMPEGLog = @file_get_contents($logfile); + + $dPos = strpos($FFMPEGLog, " Duration: "); + self::$duration = self::durationToSecs(substr($FFMPEGLog, $dPos + 11, 11)); + + $FFMPEGLog = str_replace("\r", "\n", $FFMPEGLog); + + $lines = explode("\n", $FFMPEGLog); + $line = $lines[count($lines) - 2]; + + if ($tpos = strpos($line, "time=")) { + $bpos = strpos($line, " bitrate="); + + $time = substr($line, $tpos + 5, $bpos - ($tpos + 5)); + return $time; + } + else { + return false; + } + } + } + + static function durationToSecs($durstr) { + list($hr, $min, $sec) = explode(":", $durstr); + + $secs = $hr * 3600; + $secs += $min * 60; + $secs += $sec; + + return $secs; + } +} diff --git a/3.0/modules/transcode/helpers/transcode_theme.php b/3.0/modules/transcode/helpers/transcode_theme.php new file mode 100644 index 00000000..371576ac --- /dev/null +++ b/3.0/modules/transcode/helpers/transcode_theme.php @@ -0,0 +1,19 @@ +css_id = "g-resolutions"; + $block->title = t("Alternative Resolutions"); + + $view = new View("transcode_resolution_variants.html"); + $view->item = $theme->item(); + $view->resolutions = ORM::factory("transcode_resolution")->where("item_id", "=", $view->item->id)->find_all(); + + $block->content = $view; + return $block; + } + +} \ No newline at end of file diff --git a/3.0/modules/transcode/models/transcode_resolution.php b/3.0/modules/transcode/models/transcode_resolution.php new file mode 100644 index 00000000..f02cf11c --- /dev/null +++ b/3.0/modules/transcode/models/transcode_resolution.php @@ -0,0 +1,4 @@ + + + +
+

+ +

+ +
+ +
+
+ + diff --git a/3.0/modules/transcode/views/transcode_resolution_variants.html.php b/3.0/modules/transcode/views/transcode_resolution_variants.html.php new file mode 100644 index 00000000..6c1b5256 --- /dev/null +++ b/3.0/modules/transcode/views/transcode_resolution_variants.html.php @@ -0,0 +1,29 @@ + + +is_movie()): ?> + 0): ?> + + + +

No alternative resolutions available.

+ +