| /* |
| * Copyright (c) 2012 The WebRTC project authors. All Rights Reserved. |
| * |
| * Use of this source code is governed by a BSD-style license |
| * that can be found in the LICENSE file in the root of the source |
| * tree. An additional intellectual property rights grant can be found |
| * in the file PATENTS. All contributing project authors may |
| * be found in the AUTHORS file in the root of the source tree. |
| */ |
| |
| #include "webrtc/modules/video_coding/main/test/quality_modes_test.h" |
| |
| #include <iostream> |
| #include <sstream> |
| #include <string> |
| #include <time.h> |
| |
| #include "webrtc/common_video/libyuv/include/webrtc_libyuv.h" |
| #include "webrtc/modules/video_coding/main/interface/video_coding.h" |
| #include "webrtc/modules/video_coding/main/test/test_callbacks.h" |
| #include "webrtc/modules/video_coding/main/test/test_macros.h" |
| #include "webrtc/modules/video_coding/main/test/test_util.h" |
| #include "webrtc/system_wrappers/interface/clock.h" |
| #include "webrtc/system_wrappers/interface/data_log.h" |
| #include "webrtc/system_wrappers/interface/data_log.h" |
| #include "webrtc/test/testsupport/fileutils.h" |
| #include "webrtc/test/testsupport/metrics/video_metrics.h" |
| |
| using namespace webrtc; |
| |
| int qualityModeTest(const CmdArgs& args) |
| { |
| SimulatedClock clock(0); |
| NullEventFactory event_factory; |
| VideoCodingModule* vcm = VideoCodingModule::Create(&clock, &event_factory); |
| QualityModesTest QMTest(vcm, &clock); |
| QMTest.Perform(args); |
| VideoCodingModule::Destroy(vcm); |
| return 0; |
| } |
| |
| QualityModesTest::QualityModesTest(VideoCodingModule* vcm, |
| Clock* clock): |
| NormalTest(vcm, clock), |
| _vpm() |
| { |
| // |
| } |
| |
| QualityModesTest::~QualityModesTest() |
| { |
| // |
| } |
| |
| void |
| QualityModesTest::Setup(const CmdArgs& args) |
| { |
| NormalTest::Setup(args); |
| _inname = args.inputFile; |
| _outname = args.outputFile; |
| fv_outfilename_ = args.fv_outputfile; |
| |
| filename_testvideo_ = |
| _inname.substr(_inname.find_last_of("\\/") + 1,_inname.length()); |
| |
| _encodedName = test::OutputPath() + "encoded_qmtest.yuv"; |
| |
| //NATIVE/SOURCE VALUES |
| _nativeWidth = args.width; |
| _nativeHeight = args.height; |
| _nativeFrameRate =args.frameRate; |
| |
| //TARGET/ENCODER VALUES |
| _width = args.width; |
| _height = args.height; |
| _frameRate = args.frameRate; |
| |
| _bitRate = args.bitRate; |
| |
| _flagSSIM = true; |
| |
| _lengthSourceFrame = 3*_nativeWidth*_nativeHeight/2; |
| |
| if ((_sourceFile = fopen(_inname.c_str(), "rb")) == NULL) |
| { |
| printf("Cannot read file %s.\n", _inname.c_str()); |
| exit(1); |
| } |
| if ((_encodedFile = fopen(_encodedName.c_str(), "wb")) == NULL) |
| { |
| printf("Cannot write encoded file.\n"); |
| exit(1); |
| } |
| if ((_decodedFile = fopen(_outname.c_str(), "wb")) == NULL) |
| { |
| printf("Cannot write file %s.\n", _outname.c_str()); |
| exit(1); |
| } |
| |
| DataLog::CreateLog(); |
| |
| feature_table_name_ = fv_outfilename_; |
| |
| DataLog::AddTable(feature_table_name_); |
| |
| DataLog::AddColumn(feature_table_name_, "motion magnitude", 1); |
| DataLog::AddColumn(feature_table_name_, "spatial prediction error", 1); |
| DataLog::AddColumn(feature_table_name_, "spatial pred err horizontal", 1); |
| DataLog::AddColumn(feature_table_name_, "spatial pred err vertical", 1); |
| DataLog::AddColumn(feature_table_name_, "width", 1); |
| DataLog::AddColumn(feature_table_name_, "height", 1); |
| DataLog::AddColumn(feature_table_name_, "num pixels", 1); |
| DataLog::AddColumn(feature_table_name_, "frame rate", 1); |
| DataLog::AddColumn(feature_table_name_, "num frames since drop", 1); |
| |
| _log.open((test::OutputPath() + "TestLog.txt").c_str(), |
| std::fstream::out | std::fstream::app); |
| } |
| |
| void |
| QualityModesTest::Print() |
| { |
| std::cout << "Quality Modes Test Completed!" << std::endl; |
| (_log) << "Quality Modes Test Completed!" << std::endl; |
| (_log) << "Input file: " << _inname << std::endl; |
| (_log) << "Output file: " << _outname << std::endl; |
| (_log) << "Total run time: " << _testTotalTime << std::endl; |
| printf("Total run time: %f s \n", _testTotalTime); |
| double ActualBitRate = 8.0*( _sumEncBytes / (_frameCnt / _nativeFrameRate)); |
| double actualBitRate = ActualBitRate / 1000.0; |
| double avgEncTime = _totalEncodeTime / _frameCnt; |
| double avgDecTime = _totalDecodeTime / _frameCnt; |
| webrtc::test::QualityMetricsResult psnr,ssim; |
| I420PSNRFromFiles(_inname.c_str(), _outname.c_str(), _nativeWidth, |
| _nativeHeight, &psnr); |
| printf("Actual bitrate: %f kbps\n", actualBitRate); |
| printf("Target bitrate: %f kbps\n", _bitRate); |
| ( _log) << "Actual bitrate: " << actualBitRate<< " kbps\tTarget: " << |
| _bitRate << " kbps" << std::endl; |
| printf("Average encode time: %f s\n", avgEncTime); |
| ( _log) << "Average encode time: " << avgEncTime << " s" << std::endl; |
| printf("Average decode time: %f s\n", avgDecTime); |
| ( _log) << "Average decode time: " << avgDecTime << " s" << std::endl; |
| printf("PSNR: %f \n", psnr.average); |
| printf("**Number of frames dropped in VPM***%d \n",_numFramesDroppedVPM); |
| ( _log) << "PSNR: " << psnr.average << std::endl; |
| if (_flagSSIM == 1) |
| { |
| printf("***computing SSIM***\n"); |
| I420SSIMFromFiles(_inname.c_str(), _outname.c_str(), _nativeWidth, |
| _nativeHeight, &ssim); |
| printf("SSIM: %f \n", ssim.average); |
| } |
| (_log) << std::endl; |
| |
| printf("\nVCM Quality Modes Test: \n\n%i tests completed\n", vcmMacrosTests); |
| if (vcmMacrosErrors > 0) |
| { |
| printf("%i FAILED\n\n", vcmMacrosErrors); |
| } |
| else |
| { |
| printf("ALL PASSED\n\n"); |
| } |
| } |
| void |
| QualityModesTest::Teardown() |
| { |
| _log.close(); |
| fclose(_sourceFile); |
| fclose(_decodedFile); |
| fclose(_encodedFile); |
| return; |
| } |
| |
| int32_t |
| QualityModesTest::Perform(const CmdArgs& args) |
| { |
| Setup(args); |
| // changing bit/frame rate during the test |
| const float bitRateUpdate[] = {1000}; |
| const float frameRateUpdate[] = {30}; |
| // frame num at which an update will occur |
| const int updateFrameNum[] = {10000}; |
| |
| uint32_t numChanges = sizeof(updateFrameNum)/sizeof(*updateFrameNum); |
| uint8_t change = 0;// change counter |
| |
| _vpm = VideoProcessingModule::Create(1); |
| EventWrapper* waitEvent = EventWrapper::Create(); |
| VideoCodec codec;//both send and receive |
| _vcm->InitializeReceiver(); |
| _vcm->InitializeSender(); |
| int32_t NumberOfCodecs = _vcm->NumberOfCodecs(); |
| for (int i = 0; i < NumberOfCodecs; i++) |
| { |
| _vcm->Codec(i, &codec); |
| if(strncmp(codec.plName,"VP8" , 5) == 0) |
| { |
| codec.startBitrate = (int)_bitRate; |
| codec.maxFramerate = (uint8_t) _frameRate; |
| codec.width = (uint16_t)_width; |
| codec.height = (uint16_t)_height; |
| codec.codecSpecific.VP8.frameDroppingOn = false; |
| |
| // Will also set and init the desired codec |
| TEST(_vcm->RegisterSendCodec(&codec, 2, 1440) == VCM_OK); |
| i = NumberOfCodecs; |
| } |
| } |
| |
| // register a decoder (same codec for decoder and encoder ) |
| TEST(_vcm->RegisterReceiveCodec(&codec, 2) == VCM_OK); |
| /* Callback Settings */ |
| VCMQMDecodeCompleCallback _decodeCallback( |
| _decodedFile, _nativeFrameRate, feature_table_name_); |
| _vcm->RegisterReceiveCallback(&_decodeCallback); |
| VCMNTEncodeCompleteCallback _encodeCompleteCallback(_encodedFile, *this); |
| _vcm->RegisterTransportCallback(&_encodeCompleteCallback); |
| // encode and decode with the same vcm |
| _encodeCompleteCallback.RegisterReceiverVCM(_vcm); |
| |
| //quality modes callback |
| QMTestVideoSettingsCallback QMCallback; |
| QMCallback.RegisterVCM(_vcm); |
| QMCallback.RegisterVPM(_vpm); |
| //_vcm->RegisterVideoQMCallback(&QMCallback); |
| |
| /////////////////////// |
| /// Start Test |
| /////////////////////// |
| _vpm->EnableTemporalDecimation(true); |
| _vpm->EnableContentAnalysis(true); |
| _vpm->SetInputFrameResampleMode(kFastRescaling); |
| |
| // disabling internal VCM frame dropper |
| _vcm->EnableFrameDropper(false); |
| |
| I420VideoFrame sourceFrame; |
| I420VideoFrame *decimatedFrame = NULL; |
| uint8_t* tmpBuffer = new uint8_t[_lengthSourceFrame]; |
| double startTime = clock()/(double)CLOCKS_PER_SEC; |
| _vcm->SetChannelParameters(static_cast<uint32_t>(1000 * _bitRate), 0, 0); |
| |
| SendStatsTest sendStats; |
| sendStats.set_framerate(static_cast<uint32_t>(_frameRate)); |
| sendStats.set_bitrate(1000 * _bitRate); |
| _vcm->RegisterSendStatisticsCallback(&sendStats); |
| |
| VideoContentMetrics* contentMetrics = NULL; |
| // setting user frame rate |
| // for starters: keeping native values: |
| _vpm->SetTargetResolution(_width, _height, |
| (uint32_t)(_frameRate+ 0.5f)); |
| _decodeCallback.SetOriginalFrameDimensions(_nativeWidth, _nativeHeight); |
| |
| //tmp - disabling VPM frame dropping |
| _vpm->EnableTemporalDecimation(false); |
| |
| int32_t ret = 0; |
| _numFramesDroppedVPM = 0; |
| |
| do { |
| if (fread(tmpBuffer, 1, _lengthSourceFrame, _sourceFile) > 0) { |
| _frameCnt++; |
| int size_y = _nativeWidth * _nativeHeight; |
| int size_uv = ((_nativeWidth + 1) / 2) * ((_nativeHeight + 1) / 2); |
| sourceFrame.CreateFrame(size_y, tmpBuffer, |
| size_uv, tmpBuffer + size_y, |
| size_uv, tmpBuffer + size_y + size_uv, |
| _nativeWidth, _nativeHeight, |
| _nativeWidth, (_nativeWidth + 1) / 2, |
| (_nativeWidth + 1) / 2); |
| |
| _timeStamp += |
| (uint32_t)(9e4 / static_cast<float>(codec.maxFramerate)); |
| sourceFrame.set_timestamp(_timeStamp); |
| |
| ret = _vpm->PreprocessFrame(sourceFrame, &decimatedFrame); |
| if (ret == 1) |
| { |
| printf("VD: frame drop %d \n",_frameCnt); |
| _numFramesDroppedVPM += 1; |
| continue; // frame drop |
| } |
| else if (ret < 0) |
| { |
| printf("Error in PreprocessFrame: %d\n", ret); |
| //exit(1); |
| } |
| // Frame was not re-sampled => use original. |
| if (decimatedFrame == NULL) |
| { |
| decimatedFrame = &sourceFrame; |
| } |
| contentMetrics = _vpm->ContentMetrics(); |
| if (contentMetrics == NULL) |
| { |
| printf("error: contentMetrics = NULL\n"); |
| } |
| |
| // counting only encoding time |
| _encodeTimes[int(sourceFrame.timestamp())] = |
| clock()/(double)CLOCKS_PER_SEC; |
| |
| int32_t ret = _vcm->AddVideoFrame(*decimatedFrame, contentMetrics); |
| |
| _totalEncodeTime += clock()/(double)CLOCKS_PER_SEC - |
| _encodeTimes[int(sourceFrame.timestamp())]; |
| |
| if (ret < 0) |
| { |
| printf("Error in AddFrame: %d\n", ret); |
| //exit(1); |
| } |
| |
| // Same timestamp value for encode and decode |
| _decodeTimes[int(sourceFrame.timestamp())] = |
| clock()/(double)CLOCKS_PER_SEC; |
| ret = _vcm->Decode(); |
| |
| _totalDecodeTime += clock()/(double)CLOCKS_PER_SEC - |
| _decodeTimes[int(sourceFrame.timestamp())]; |
| |
| if (ret < 0) |
| { |
| printf("Error in Decode: %d\n", ret); |
| //exit(1); |
| } |
| if (_vcm->TimeUntilNextProcess() <= 0) |
| { |
| _vcm->Process(); |
| } |
| // mimicking setTargetRates - update every 1 sec |
| // this will trigger QMSelect |
| if (_frameCnt%((int)_frameRate) == 0) |
| { |
| _vcm->SetChannelParameters(static_cast<uint32_t>(1000 * _bitRate), 0, |
| 1); |
| } |
| |
| // check for bit rate update |
| if (change < numChanges && _frameCnt == updateFrameNum[change]) |
| { |
| _bitRate = bitRateUpdate[change]; |
| _frameRate = frameRateUpdate[change]; |
| codec.startBitrate = (int)_bitRate; |
| codec.maxFramerate = (uint8_t) _frameRate; |
| // Will also set and init the desired codec |
| TEST(_vcm->RegisterSendCodec(&codec, 2, 1440) == VCM_OK); |
| change++; |
| } |
| |
| DataLog::InsertCell(feature_table_name_, "motion magnitude", |
| contentMetrics->motion_magnitude); |
| DataLog::InsertCell(feature_table_name_, "spatial prediction error", |
| contentMetrics->spatial_pred_err); |
| DataLog::InsertCell(feature_table_name_, "spatial pred err horizontal", |
| contentMetrics->spatial_pred_err_h); |
| DataLog::InsertCell(feature_table_name_, "spatial pred err vertical", |
| contentMetrics->spatial_pred_err_v); |
| |
| DataLog::InsertCell(feature_table_name_, "width", _nativeHeight); |
| DataLog::InsertCell(feature_table_name_, "height", _nativeWidth); |
| |
| DataLog::InsertCell(feature_table_name_, "num pixels", |
| _nativeHeight * _nativeWidth); |
| |
| DataLog::InsertCell(feature_table_name_, "frame rate", _nativeFrameRate); |
| DataLog::NextRow(feature_table_name_); |
| |
| static_cast<SimulatedClock*>(_clock)->AdvanceTimeMilliseconds( |
| 1000 / _nativeFrameRate); |
| } |
| |
| } while (feof(_sourceFile) == 0); |
| _decodeCallback.WriteEnd(_frameCnt); |
| |
| |
| double endTime = clock()/(double)CLOCKS_PER_SEC; |
| _testTotalTime = endTime - startTime; |
| _sumEncBytes = _encodeCompleteCallback.EncodedBytes(); |
| |
| delete tmpBuffer; |
| delete waitEvent; |
| _vpm->Reset(); |
| Teardown(); |
| Print(); |
| VideoProcessingModule::Destroy(_vpm); |
| return 0; |
| } |
| |
| // implementing callback to be called from |
| // VCM to update VPM of frame rate and size |
| QMTestVideoSettingsCallback::QMTestVideoSettingsCallback(): |
| _vpm(NULL), |
| _vcm(NULL) |
| { |
| // |
| } |
| |
| void |
| QMTestVideoSettingsCallback::RegisterVPM(VideoProcessingModule *vpm) |
| { |
| _vpm = vpm; |
| } |
| void |
| QMTestVideoSettingsCallback::RegisterVCM(VideoCodingModule *vcm) |
| { |
| _vcm = vcm; |
| } |
| |
| bool |
| QMTestVideoSettingsCallback::Updated() |
| { |
| if (_updated) |
| { |
| _updated = false; |
| return true; |
| } |
| return false; |
| } |
| |
| int32_t |
| QMTestVideoSettingsCallback::SetVideoQMSettings(const uint32_t frameRate, |
| const uint32_t width, |
| const uint32_t height) |
| { |
| int32_t retVal = 0; |
| printf("QM updates: W = %d, H = %d, FR = %d, \n", width, height, frameRate); |
| retVal = _vpm->SetTargetResolution(width, height, frameRate); |
| //Initialize codec with new values - is this the best place to do it? |
| if (!retVal) |
| { |
| // first get current settings |
| VideoCodec currentCodec; |
| _vcm->SendCodec(¤tCodec); |
| // now set new values: |
| currentCodec.height = (uint16_t)height; |
| currentCodec.width = (uint16_t)width; |
| currentCodec.maxFramerate = (uint8_t)frameRate; |
| |
| // re-register encoder |
| retVal = _vcm->RegisterSendCodec(¤tCodec, 2, 1440); |
| _updated = true; |
| } |
| |
| return retVal; |
| } |
| |
| // Decoded Frame Callback Implementation |
| VCMQMDecodeCompleCallback::VCMQMDecodeCompleCallback( |
| FILE* decodedFile, int frame_rate, std::string feature_table_name): |
| _decodedFile(decodedFile), |
| _decodedBytes(0), |
| //_test(test), |
| _origWidth(0), |
| _origHeight(0), |
| _decWidth(0), |
| _decHeight(0), |
| //_interpolator(NULL), |
| _decBuffer(NULL), |
| _frameCnt(0), |
| frame_rate_(frame_rate), |
| frames_cnt_since_drop_(0), |
| feature_table_name_(feature_table_name) |
| { |
| // |
| } |
| |
| VCMQMDecodeCompleCallback::~VCMQMDecodeCompleCallback() |
| { |
| // if (_interpolator != NULL) |
| // { |
| // deleteInterpolator(_interpolator); |
| // _interpolator = NULL; |
| // } |
| if (_decBuffer != NULL) |
| { |
| delete [] _decBuffer; |
| _decBuffer = NULL; |
| } |
| } |
| |
| int32_t |
| VCMQMDecodeCompleCallback::FrameToRender(I420VideoFrame& videoFrame) |
| { |
| ++frames_cnt_since_drop_; |
| |
| // When receiving the first coded frame the last_frame variable is not set |
| if (last_frame_.IsZeroSize()) { |
| last_frame_.CopyFrame(videoFrame); |
| } |
| |
| // Check if there were frames skipped. |
| int num_frames_skipped = static_cast<int>( 0.5f + |
| (videoFrame.timestamp() - (last_frame_.timestamp() + (9e4 / frame_rate_))) / |
| (9e4 / frame_rate_)); |
| |
| // If so...put the last frames into the encoded stream to make up for the |
| // skipped frame(s) |
| while (num_frames_skipped > 0) { |
| PrintI420VideoFrame(last_frame_, _decodedFile); |
| _frameCnt++; |
| --num_frames_skipped; |
| frames_cnt_since_drop_ = 1; // Reset counter |
| |
| } |
| |
| DataLog::InsertCell( |
| feature_table_name_,"num frames since drop",frames_cnt_since_drop_); |
| |
| if (_origWidth == videoFrame.width() && _origHeight == videoFrame.height()) |
| { |
| if (PrintI420VideoFrame(videoFrame, _decodedFile) < 0) { |
| return -1; |
| } |
| _frameCnt++; |
| // no need for interpolator and decBuffer |
| if (_decBuffer != NULL) |
| { |
| delete [] _decBuffer; |
| _decBuffer = NULL; |
| } |
| _decWidth = 0; |
| _decHeight = 0; |
| } |
| else |
| { |
| // TODO(mikhal): Add support for scaling. |
| return -1; |
| } |
| |
| _decodedBytes += CalcBufferSize(kI420, videoFrame.width(), |
| videoFrame.height()); |
| videoFrame.SwapFrame(&last_frame_); |
| return VCM_OK; |
| } |
| |
| int32_t VCMQMDecodeCompleCallback::DecodedBytes() |
| { |
| return _decodedBytes; |
| } |
| |
| void VCMQMDecodeCompleCallback::SetOriginalFrameDimensions(int32_t width, |
| int32_t height) |
| { |
| _origWidth = width; |
| _origHeight = height; |
| } |
| |
| int32_t VCMQMDecodeCompleCallback::buildInterpolator() |
| { |
| uint32_t decFrameLength = _origWidth*_origHeight*3 >> 1; |
| if (_decBuffer != NULL) |
| { |
| delete [] _decBuffer; |
| } |
| _decBuffer = new uint8_t[decFrameLength]; |
| if (_decBuffer == NULL) |
| { |
| return -1; |
| } |
| return 0; |
| } |
| |
| // This function checks if the total number of frames processed in the encoding |
| // process is the same as the number of frames rendered. If not, the last |
| // frame (or several consecutive frames from the end) must have been dropped. If |
| // this is the case, the last frame is repeated so that there are as many |
| // frames rendered as there are number of frames encoded. |
| void VCMQMDecodeCompleCallback::WriteEnd(int input_frame_count) |
| { |
| int num_missing_frames = input_frame_count - _frameCnt; |
| |
| for (int n = num_missing_frames; n > 0; --n) { |
| PrintI420VideoFrame(last_frame_, _decodedFile); |
| _frameCnt++; |
| } |
| } |