/** * Check for invalidly generated content files - * - Silent or black content for at least 50% of the total duration * - The detection duration - at least 2 sec * - Applicable only to Webex sources * @param KalturaBatchJob $job * @param KalturaPostConvertJobData $data * $param $mediaFile * #param KalturaMediaInfo $mediaInfo * @return boolean */ private function checkForValidityOfWebexProduct(KalturaPostConvertJobData $data, $srcFileName, KalturaMediaInfo $mediaInfo, &$detectMsg) { $rv = true; $detectMsg = null; /* * Get silent and black portions * list($silenceDetect, $blackDetect) = KFFMpegMediaParser::checkForSilentAudioAndBlackVideo(KBatchBase::$taskConfig->params->FFMpegCmd, $srcFileName, $mediaInfo); $detectMsg = $silenceDetect; if(isset($blackDetect)) $detectMsg = isset($detectMsg)?"$detectMsg,$blackDetect":$blackDetect; */ /* * Silent/Black does not cause validation failure, just a job message */ if (isset($detectMsg)) { // return false; } /* * Get number of Webex operators that represent the number of conversion retries. * Return success after the last retry, independently of whether the result is garbled or not. * The assumption is that 3 retries will bring the number of garbled audios to acceptable rate. * Therefore if the audio is still garbled, it is probably due to false detection, * therefore DO NOT fail the asset. */ $operators = json_decode($data->flavorParamsOutput->operators); if ($data->currentOperationSet < count($operators) - 1) { if (KFFMpegMediaParser::checkForGarbledAudio(KBatchBase::$taskConfig->params->FFMpegCmd, $srcFileName, $mediaInfo) == true) { $detectMsg .= " Garbled Audio!"; $rv = false; } } return $rv; }
protected function convertRecordedToMPEGTS($ffmpegBin, $ffprobeBin, $inFilename, $outFilename) { $cmdStr = "{$ffmpegBin} -i {$inFilename} -c copy -bsf:v h264_mp4toannexb -f mpegts -y {$outFilename} 2>&1"; KalturaLog::debug("Executing [{$cmdStr}]"); $output = system($cmdStr, $rv); /* * Anomaly detection - * Look for the time of the first KF in the source file. * Should be less than 200 msec * Currnetly - just logging */ $detectInterval = 10; // sec $maxKeyFrameTime = 0.2; // sec $kfArr = KFFMpegMediaParser::retrieveKeyFrames($ffprobeBin, $inFilename, 0, $detectInterval); KalturaLog::log("KeyFrames:" . print_r($kfArr, 1)); if (count($kfArr) == 0) { KalturaLog::log("Anomaly detection: NO Keyframes in the detection interval ({$detectInterval} sec)"); } else { if ($kfArr[0] > $maxKeyFrameTime) { KalturaLog::log("Anomaly detection: ERROR, first KF at ({$kfArr['0']} sec), max allowed ({$maxKeyFrameTime} sec)"); } else { KalturaLog::log("Anomaly detection: OK, first KF at ({$kfArr['0']} sec), max allowed ({$maxKeyFrameTime} sec)"); } } return $rv == 0 ? true : false; }
/** * @return KalturaMediaInfo */ public function getMediaInfo() { /* * KFFMpegMediaParser is activated here as a fall back to mediainfo for M1S * and for test reasons prior to switching from mediainfo to ffprobe */ $ffParser = new KFFMpegMediaParser($this->filePath); //, "ffmpeg-20140326", "ffprobe-20140326"); $ffMi = null; try { $ffMi = $ffParser->getMediaInfo(); } catch (Exception $ex) { KalturaLog::log(print_r($ex, 1)); } $output = $this->getRawMediaInfo(); $kMi = $this->parseOutput($output); if (!isset($kMi)) { $compareStr = self::compareFields($kMi, $ffMi); KalturaLog::log("compareFields(" . (isset($compareStr) ? $compareStr : "IDENTICAL") . "), file({$this->filePath})"); return $ffMi; } /* * Interlaced mjpa sources - the height value is halved. */ if (isset($kMi->videoHeightTmp) && isset($kMi->scanType) && $kMi->scanType == 1) { $kMi->videoHeight = $kMi->videoHeightTmp; } /* * WebM/VP8 misses video duration */ if (isset($kMi->videoFormat) && $kMi->videoFormat == "vp8" && (!isset($kMi->videoDuration) || $kMi->videoDuration == 0)) { $kMi->videoDuration = $kMi->containerDuration; } $durLimit = 3600000; if (get_class($this) == 'KMediaInfoMediaParser' && (isset($kMi->containerDuration) && $kMi->containerDuration >= $durLimit || isset($kMi->videoDuration) && $kMi->videoDuration >= $durLimit || isset($kMi->audioDuration) && $kMi->audioDuration >= $durLimit)) { $cmd = "{$this->cmdPath} \"--Inform=General;done %Duration%\" \"{$this->filePath}\""; $output = 0; $output = shell_exec($cmd); $aux = explode(" ", trim($output)); if (isset($aux) && count($aux) == 2 && $aux[0] == 'done') { $kMi->containerDuration = (int) $aux[1]; } $cmd = "{$this->cmdPath} \"--Inform=Video;done %Duration%\" \"{$this->filePath}\""; $output = 0; $output = shell_exec($cmd); $aux = explode(" ", trim($output)); if (isset($aux) && count($aux) == 2 && $aux[0] == 'done') { $kMi->videoDuration = (int) $aux[1]; } $cmd = "{$this->cmdPath} \"--Inform=Audio;done %Duration%\" \"{$this->filePath}\""; $output = 0; $output = shell_exec($cmd); $aux = explode(" ", trim($output)); if (isset($aux) && count($aux) == 2 && $aux[0] == 'done') { $kMi->audioDuration = (int) $aux[1]; } } if (isset($ffMi)) { /* * Media info's vid/aud streams are unset - use object that was generated by ffprobe, * unless it is an ARF source. */ if (!self::isAudioSet($kMi) && !self::isVideoSet($kMi) && $kMi->containerFormat != "arf") { $compareStr = self::compareFields($kMi, $ffMi); KalturaLog::log("compareFields(" . (isset($compareStr) ? $compareStr : "IDENTICAL") . "), file({$this->filePath})"); return $ffMi; } /* * On off-sanity wid/height - use ffprobe object vals (overwrite the dar too) */ if (isset($kMi->videoWidth) && isset($kMi->videoHeight) && ($kMi->videoWidth > KDLSanityLimits::MaxDimension || $kMi->videoWidth < KDLSanityLimits::MinDimension || $kMi->videoHeight > KDLSanityLimits::MaxDimension || $kMi->videoHeight < KDLSanityLimits::MinDimension)) { if (isset($ffMi->videoWidth) && isset($ffMi->videoHeight) && !($ffMi->videoWidth > KDLSanityLimits::MaxDimension || $ffMi->videoWidth < KDLSanityLimits::MinDimension || $ffMi->videoHeight > KDLSanityLimits::MaxDimension || $ffMi->videoHeight < KDLSanityLimits::MinDimension)) { $kMi->videoWidth = $ffMi->videoWidth; $kMi->videoHeight = $ffMi->videoHeight; if (isset($ffMi->videoDar)) { $kMi->videoDar = $ffMi->videoDar; } } } /* * On off-sanity dar or if the is AR ambiguity, due to 'original dar' * - use ffprobe object dar */ if (isset($kMi->videoDar) && ($kMi->videoDar > KDLSanityLimits::MaxDAR || $kMi->videoDar < KDLSanityLimits::MinDAR || isset($kMi->originalDar))) { if (isset($ffMi->videoDar) && !($ffMi->videoDar > KDLSanityLimits::MaxDAR || $ffMi->videoDar < KDLSanityLimits::MinDAR)) { $kMi->videoDar = $ffMi->videoDar; } } /* * Update mediainfo generated object with fastStart and contentStreams fields * that are available only on ffprobe */ $kMi->isFastStart = $ffMi->isFastStart; $kMi->contentStreams = $ffMi->contentStreams; } $compareStr = self::compareFields($kMi, $ffMi); KalturaLog::log("compareFields(" . (isset($compareStr) ? $compareStr : "IDENTICAL") . "), file({$this->filePath})"); return $kMi; }
/** * * @param unknown_type $ffmpegBin * @param unknown_type $ffprobeBin * @param array $filesArr * @param unknown_type $outFilename * @param unknown_type $clipStart * @param unknown_type $clipDuration * @return boolean */ protected static function concatFiles($ffmpegBin, $ffprobeBin, array $filesArr, $outFilename, $clipStart = null, $clipDuration = null) { $fixLargeDeltaFlag = null; $chunkBr = null; $concateStr = null; /* * Evaluate clipping arguments */ $clipStr = null; if (isset($clipStart)) { $clipStr = "-ss {$clipStart}"; } if (isset($clipDuration)) { $clipStr .= " -t {$clipDuration}"; } sort($filesArr); $filesArrCnt = count($filesArr); $i = 0; $mi = null; foreach ($filesArr as $fileName) { $i++; /* * Get chunk file media-info */ $ffParser = new KFFMpegMediaParser($fileName, $ffmpegBin, $ffprobeBin); $mi = null; try { $mi = $ffParser->getMediaInfo(); } catch (Exception $ex) { KalturaLog::log(print_r($ex, 1)); } /* * Calculate chunk-br for the cliping flow */ if (isset($clipStr)) { if (isset($mi->containerBitRate) && $mi->containerBitRate > 0) { $chunkBr += $mi->containerBitRate; } else { if (isset($mi->videoBitRate) && $mi->videoBitRate > 0) { $chunkBr += $mi->videoBitRate; } else { if (isset($mi->audioBitRate) && $mi->audioBitRate > 0) { $chunkBr += $mi->audioBitRate; } } } } /* * On last chunk file - * - no duration delta validity tests * - pack the final concat string and finish the loop */ if ($i == $filesArrCnt) { $concateStr = "concat:\"{$concateStr}{$fileName}\""; break; } else { $concateStr .= "{$fileName}|"; } /* * ############## * ############## DISABLE the 'small-distortion-fix' code * ############## There were cases when the the 'fix' cuased another distortion * ############## Hopefully WWZ fixed their chunks generation procedure in 4.1.2 * ############## If it does - all this code/remark should be removed, * ############## otherwise (in case that the fix will still be required) - it should be enhanced. * ############## * Evaluate chunk duration for drift validation * - only one duration anomaly is required to set the drift fix flag, no need to check following chunk files * if(!isset($fixLargeDeltaFlag)) { if(isset($mi->containerDuration) && $mi->containerDuration>0) $duration = $mi->containerDuration; else if(isset($mi->videoDuration) && $mi->videoDuration>0) $duration = $mi->videoDuration; else if(isset($mi->audioDuration) && $mi->audioDuration>0) $duration = $mi->audioDuration; else $duration = 0; if($duration>0){ * * If the duration is too small - stop/start flow, don't fix * if(KAsyncConcat::LiveChunkDuration-$duration>30000){ $fixLargeDeltaFlag = false; } else if(abs($duration-KAsyncConcat::LiveChunkDuration)>KAsyncConcat::MaxChunkDelta){ $fixLargeDeltaFlag = true; } } KalturaLog::log("Chunk duration($duration), Wowza chunk setting(".KAsyncConcat::LiveChunkDuration."),max-allowed-delta(".KAsyncConcat::MaxChunkDelta."),fixLargeDeltaFlag($fixLargeDeltaFlag) "); } */ } /* * For clip flow - set converion to x264, * otherwise - just copy video */ if (isset($clipStr)) { $videoParamStr = "-c:v libx264"; if (isset($chunkBr) && $chunkBr > 0 && $filesArrCnt > 0) { $chunkBr = round($chunkBr / $filesArrCnt); $videoParamStr .= " -b:v {$chunkBr}" . "k"; } $videoParamStr .= " -subq 7 -qcomp 0.6 -qmin 10 -qmax 50 -qdiff 4 -bf 16 -coder 1 -refs 6 -x264opts b-pyramid:weightb:mixed-refs:8x8dct:no-fast-pskip=0 -vprofile high -pix_fmt yuv420p -threads 4"; } else { $videoParamStr = "-c:v copy"; } /* * If no audio - skip. * For AAC source - copy audio, * otherwise - convert to AAC */ $audioParamStr = null; if (isset($mi->audioFormat) || isset($mi->audioCodecId) || isset($mi->audioDuration)) { if (isset($mi->audioFormat) && $mi->audioFormat == "aac") { $audioParamStr = "-c:a copy"; } else { $audioParamStr = "-c:a libfdk_aac"; } $audioParamStr .= " -bsf:a aac_adtstoasc"; } $cmdStr = "{$ffmpegBin} -probesize 15M -analyzeduration 25M -i {$concateStr} {$videoParamStr} {$audioParamStr}"; $cmdStr .= " {$clipStr} -f mp4 -y {$outFilename} 2>&1"; KalturaLog::debug("Executing [{$cmdStr}]"); $output = system($cmdStr, $rv); return $rv == 0 ? true : false; }
/** * * @param unknown_type $cmdStr * @param unknown_type $flavorParamsOutput * @param unknown_type $binCmd * @param unknown_type $srcFilePath * @param unknown_type $outFilePath * @return unknown|string */ public static function experimentalFixing($cmdStr, $flavorParamsOutput, $binCmd, $srcFilePath, $outFilePath) { /* * Samples - * Original * ffmpeg -i SOURCE * -c:v libx265 * -pix_fmt yuv420p -aspect 640:360 -b:v 8000k -s 640x360 -r 30 -g 60 * -c:a libfdk_aac -b:a 128k -ar 44100 -f mp4 -y OUTPUT * * Switched - * ffmpeg -i SOURCE * -pix_fmt yuv420p -aspect 640:360 -b:v 8000k -s 640x360 -r 30 -g 60 -f yuv4mpegpipe -an - * -vn * -c:a libfdk_aac -b:a 128k -ar 44100 -f mp4 -y OUTPUT.aac * | /home/dev/x265 - --y4m --scenecut 40 --keyint 60 --min-keyint 1 --bitrate 2000 --qpfile OUTPUT.qp OUTPUT.h265 * && ~/ffmpeg-2.4.3 -i OUTPUT.aac -r 30 -i OUTPUT.h265 -c copy -f mp4 -y OUTPUT * */ /* * New binaries/aliases on transcoding servers */ $x265bin = "x265"; $ffmpegExperimBin = "ffmpeg-experim"; if ($flavorParamsOutput->videoCodec == KDLVideoTarget::H265) { //video_codec !!!flavorParamsOutput->videoCodec KalturaLog::log("trying to fix H265 conversion"); $gop = $flavorParamsOutput->gopSize; //gop_size !!!$flavorParamsOutput->gopSize; $vBr = $flavorParamsOutput->videoBitrate; //video_bitrate !!!$flavorParamsOutput->videoBitrate; $frameRate = $flavorParamsOutput->frameRate; //frame_rate !!!$flavorParamsOutput->frameRate; $threads = 4; $pixFmt = "yuv420p"; $cmdValsArr = explode(' ', $cmdStr); /* * Rearrange the ffmpeg cmd-line into a complex pipe and multiple command * - ffmpeg transcodes audio into an output.AAC file and decodes video into a raw resized video to be piped * - into x265 that encodes raw output.h265 * - upon completion- mux into an out.mp4 * * To Do's * - support other audio * - support other formats * */ /* * remove video codec */ if (in_array('-c:v', $cmdValsArr)) { $key = array_search('-c:v', $cmdValsArr); unset($cmdValsArr[$key + 1]); unset($cmdValsArr[$key]); } if (in_array('-threads', $cmdValsArr)) { $key = array_search('-threads', $cmdValsArr); $threads = $cmdValsArr[$key + 1]; } /* * add dual stream generation */ if (in_array('-c:a', $cmdValsArr)) { $key = array_search('-c:a', $cmdValsArr); $cmdValsArr[$key] = "-f yuv4mpegpipe -an - -vn -c:a"; } /* * handle pix-format (main vs main10) */ if (in_array('-pix_fmt', $cmdValsArr)) { $key = array_search('-pix_fmt', $cmdValsArr); $pixFmt = $cmdValsArr[$key + 1]; } switch ($pixFmt) { case "yuv420p10": case "yuv422p": $profile = "main10"; break; case "yuv420p": default: $profile = "main"; break; } /* * Get source duration */ $ffParser = new KFFMpegMediaParser($srcFilePath); $ffMi = null; try { $ffMi = $ffParser->getMediaInfo(); } catch (Exception $ex) { KalturaLog::log(print_r($ex, 1)); } if (isset($ffMi->containerDuration) && $ffMi->containerDuration > 0) { $duration = $ffMi->containerDuration / 1000; } else { if (isset($ffMi->videoDuration) && $ffMi->videoDuration > 0) { $duration = $ffMi->videoDuration / 1000; } else { if (isset($ffMi->audioDuration) && $ffMi->audioDuration > 0) { $duration = $ffMi->audioDuration / 1000; } else { $duration = 0; } } } $keyFramesArr = array(); /* * Generate x265 qpfile with forced key-frames */ if (isset($gop) && $gop > 0 && isset($frameRate) && $frameRate > 0 && isset($duration) && $duration > 0) { $gopInSec = $gop / round($frameRate); $frameDur = 1 / $frameRate; for ($kfTime = 0, $kfId = 0, $kfTimeGop = 0; $kfTime < $duration;) { $keyFramesArr[] = $kfId; $kfId += $gop; $kfTime = $kfId * $frameDur; $kfTimeGop += $gopInSec; $kfTimeDelta = $kfTime - $kfTimeGop; /* * Check for time derift conditions (for float fps, 29.97/23.947/etc) and fix when required */ if (abs($kfTimeDelta) > $frameDur) { $aaa = $kfId; if ($kfTimeDelta > 0) { $kfId--; } else { $kfId++; } } } $keyFramesStr = implode(" I\n", $keyFramesArr) . " I\n"; file_put_contents("{$outFilePath}.qp", $keyFramesStr); } else { KalturaLog::log("Missing gop({$gop}) or frameRate({$frameRate}) or duration({$duration}) - will be generated without fixed keyframes!"); } if (!in_array($outFilePath, $cmdValsArr)) { return $cmdStr; } $key = array_search($outFilePath, $cmdValsArr); $cmdValsArr[$key] = "{$outFilePath}.aac |"; $cmdValsArr[$key] .= " {$x265bin} - --profile {$profile} --y4m --scenecut 40 --min-keyint 1"; if (isset($gop)) { $cmdValsArr[$key] .= " --keyint {$gop}"; } if (isset($vBr)) { $cmdValsArr[$key] .= " --bitrate {$vBr}"; } if (count($keyFramesArr) > 0) { $cmdValsArr[$key] .= " --qpfile {$outFilePath}.qp"; } $cmdValsArr[$key] .= " --threads {$threads} {$outFilePath}.h265"; $cmdValsArr[$key] .= " && {$ffmpegExperimBin} -i {$outFilePath}.aac -r {$frameRate} -i {$outFilePath}.h265 -c copy -f mp4 -y {$outFilePath}"; $cmdStr = implode(" ", $cmdValsArr); } else { if ($flavorParamsOutput->videoCodec == KDLVideoTarget::VP9) { //video_codec ||!flavorParamsOutput->videoCodec $cmdValsArr = explode(' ', $cmdStr); $cmdValsArr[0] = $ffmpegExperimBin; $cmdStr = implode(" ", $cmdValsArr); } } return $cmdStr; }
/** * * @param unknown_type $ffmpegBin * @param unknown_type $srcFileName * @param KalturaMediaInfo $mediaInfo * @return boolean */ public static function checkForGarbledAudio($ffmpegBin, $srcFileName, KalturaMediaInfo $mediaInfo) { KalturaLog::log("contDur:{$mediaInfo->containerDuration},audDur:{$mediaInfo->audioDuration}"); if (isset($mediaInfo->audioDuration)) { $audDetectDur = $mediaInfo->audioDuration > 600000 ? 600 : round($mediaInfo->audioDuration / 1000, 2); } else { if (isset($mediaInfo->containerDuration)) { $audDetectDur = $mediaInfo->containerDuration > 600000 ? 600 : round($mediaInfo->containerDuration / 1000, 2); } else { if (isset($mediaInfo->videoDuration)) { $audDetectDur = $mediaInfo->videoDuration > 600000 ? 600 : round($mediaInfo->videoDuration / 1000, 2); } else { $audDetectDur = 0; } } } if ($audDetectDur > 0 && $audDetectDur < 10) { KalturaLog::log("Audio OK - short audio, audDetectDur({$audDetectDur})"); return false; } list($silenceDetected, $blackDetected) = KFFMpegMediaParser::detectSilentAudioAndBlackVideoIntervals($ffmpegBin, $srcFileName, null, 0.05, $audDetectDur, "-90dB"); $ticks = isset($silenceDetected) ? count($silenceDetected) : 0; if ($ticks <= 10) { KalturaLog::log("Audio OK - low numbers of ticks({$ticks})"); return false; } KalturaLog::log("audDetectDur({$audDetectDur}),ticks({$ticks})"); if ($audDetectDur > 0) { $ticksPerMin = $ticks / ($audDetectDur / 60); KalturaLog::log("ticksPerMin({$ticksPerMin})"); if ($ticksPerMin < 15 || $audDetectDur < 60 && $ticksPerMin < 30 || $audDetectDur < 120 && $ticksPerMin < 20) { KalturaLog::log("Audio OK"); return false; } } else { if ($ticks < 100) { KalturaLog::log("Audio OK - no duration, number of ticks smaller than threshold(100)"); return false; } } KalturaLog::log("Detected garbled audio."); return true; }
/** * extractFfmpegInfo extract the file info using FFmpeg and parse the returned data * * @param string $mediaFile file full path * @return KalturaMediaInfo or null for failure */ private function extractFfmpegInfo($mediaFile) { KalturaLog::debug("extractFfmpegInfo({$mediaFile})"); $mediaParser = new KFFMpegMediaParser($mediaFile, $this->taskConfig->params->FFMpegCmd); return $mediaParser->getMediaInfo(); }