1// Copyright (c) 2012 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5// Software qualification test for FFmpeg. This test is used to certify that 6// software decoding quality and performance of FFmpeg meets a mimimum 7// standard. 8 9#include <iomanip> 10#include <iostream> 11#include <string> 12 13#include "base/at_exit.h" 14#include "base/basictypes.h" 15#include "base/command_line.h" 16#include "base/file_util.h" 17#include "base/files/file_path.h" 18#include "base/files/memory_mapped_file.h" 19#include "base/logging.h" 20#include "base/md5.h" 21#include "base/path_service.h" 22#include "base/strings/string_util.h" 23#include "base/strings/utf_string_conversions.h" 24#include "base/time/time.h" 25#include "media/base/djb2.h" 26#include "media/base/media.h" 27#include "media/ffmpeg/ffmpeg_common.h" 28#include "media/filters/ffmpeg_glue.h" 29#include "media/filters/ffmpeg_video_decoder.h" 30#include "media/filters/in_memory_url_protocol.h" 31 32#ifdef DEBUG 33#define SHOW_VERBOSE 1 34#else 35#define SHOW_VERBOSE 0 36#endif 37 38#if defined(OS_WIN) 39 40// Enable to build with exception handler 41//#define ENABLE_WINDOWS_EXCEPTIONS 1 42 43#ifdef ENABLE_WINDOWS_EXCEPTIONS 44// warning: disable warning about exception handler. 45#pragma warning(disable:4509) 46#endif 47 48// Thread priorities to make benchmark more stable. 49 50void EnterTimingSection() { 51 SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_ABOVE_NORMAL); 52} 53 54void LeaveTimingSection() { 55 SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_NORMAL); 56} 57#else 58void EnterTimingSection() { 59 pthread_attr_t pta; 60 struct sched_param param; 61 62 pthread_attr_init(&pta); 63 memset(¶m, 0, sizeof(param)); 64 param.sched_priority = 78; 65 pthread_attr_setschedparam(&pta, ¶m); 66 pthread_attr_destroy(&pta); 67} 68 69void LeaveTimingSection() { 70} 71#endif 72 73int main(int argc, const char** argv) { 74 base::AtExitManager exit_manager; 75 76 CommandLine::Init(argc, argv); 77 const CommandLine* cmd_line = CommandLine::ForCurrentProcess(); 78 79 const CommandLine::StringVector& filenames = cmd_line->GetArgs(); 80 81 if (filenames.empty()) { 82 std::cerr << "Usage: " << argv[0] << " MEDIAFILE" << std::endl; 83 return 1; 84 } 85 86 // Initialize our media library (try loading DLLs, etc.) before continuing. 87 base::FilePath media_path; 88 PathService::Get(base::DIR_MODULE, &media_path); 89 if (!media::InitializeMediaLibrary(media_path)) { 90 std::cerr << "Unable to initialize the media library."; 91 return 1; 92 } 93 94 // Retrieve command line options. 95 base::FilePath in_path(filenames[0]); 96 base::FilePath out_path; 97 if (filenames.size() > 1) 98 out_path = base::FilePath(filenames[1]); 99 100 // Default flags that match Chrome defaults. 101 int video_threads = 2; 102 int max_frames = 0; 103 int max_loops = 0; 104 bool flush = false; 105 106 unsigned int hash_value = 5381u; // Seed for DJB2. 107 bool hash_djb2 = false; 108 109 base::MD5Context ctx; // Intermediate MD5 data: do not use 110 base::MD5Init(&ctx); 111 bool hash_md5 = false; 112 113 std::ostream* log_out = &std::cout; 114#if defined(ENABLE_WINDOWS_EXCEPTIONS) 115 // Catch exceptions so this tool can be used in automated testing. 116 __try { 117#endif 118 119 base::MemoryMappedFile file_data; 120 file_data.Initialize(in_path); 121 media::InMemoryUrlProtocol protocol( 122 file_data.data(), file_data.length(), false); 123 124 // Register FFmpeg and attempt to open file. 125 media::FFmpegGlue glue(&protocol); 126 if (!glue.OpenContext()) { 127 std::cerr << "Error: Could not open input for " 128 << in_path.value() << std::endl; 129 return 1; 130 } 131 132 AVFormatContext* format_context = glue.format_context(); 133 134 // Open output file. 135 FILE *output = NULL; 136 if (!out_path.empty()) { 137 output = file_util::OpenFile(out_path, "wb"); 138 if (!output) { 139 std::cerr << "Error: Could not open output " 140 << out_path.value() << std::endl; 141 return 1; 142 } 143 } 144 145 // Parse a little bit of the stream to fill out the format context. 146 if (avformat_find_stream_info(format_context, NULL) < 0) { 147 std::cerr << "Error: Could not find stream info for " 148 << in_path.value() << std::endl; 149 return 1; 150 } 151 152 // Find our target stream(s) 153 int video_stream = -1; 154 int audio_stream = -1; 155 for (size_t i = 0; i < format_context->nb_streams; ++i) { 156 AVCodecContext* codec_context = format_context->streams[i]->codec; 157 158 if (codec_context->codec_type == AVMEDIA_TYPE_VIDEO && video_stream < 0) { 159#if SHOW_VERBOSE 160 *log_out << "V "; 161#endif 162 video_stream = i; 163 } else { 164 if (codec_context->codec_type == AVMEDIA_TYPE_AUDIO && audio_stream < 0) { 165#if SHOW_VERBOSE 166 *log_out << "A "; 167#endif 168 audio_stream = i; 169 } else { 170#if SHOW_VERBOSE 171 *log_out << " "; 172#endif 173 } 174 } 175 176#if SHOW_VERBOSE 177 AVCodec* codec = avcodec_find_decoder(codec_context->codec_id); 178 if (!codec || (codec_context->codec_type == AVMEDIA_TYPE_UNKNOWN)) { 179 *log_out << "Stream #" << i << ": Unknown" << std::endl; 180 } else { 181 // Print out stream information 182 *log_out << "Stream #" << i << ": " << codec->name << " (" 183 << codec->long_name << ")" << std::endl; 184 } 185#endif 186 } 187 int target_stream = video_stream; 188 AVMediaType target_codec = AVMEDIA_TYPE_VIDEO; 189 if (target_stream < 0) { 190 target_stream = audio_stream; 191 target_codec = AVMEDIA_TYPE_AUDIO; 192 } 193 194 // Only continue if we found our target stream. 195 if (target_stream < 0) { 196 std::cerr << "Error: Could not find target stream " 197 << target_stream << " for " << in_path.value() << std::endl; 198 return 1; 199 } 200 201 // Prepare FFmpeg structures. 202 AVPacket packet; 203 AVCodecContext* codec_context = format_context->streams[target_stream]->codec; 204 AVCodec* codec = avcodec_find_decoder(codec_context->codec_id); 205 206 // Only continue if we found our codec. 207 if (!codec) { 208 std::cerr << "Error: Could not find codec for " 209 << in_path.value() << std::endl; 210 return 1; 211 } 212 213 codec_context->error_concealment = FF_EC_GUESS_MVS | FF_EC_DEBLOCK; 214 215 // Initialize threaded decode. 216 if (target_codec == AVMEDIA_TYPE_VIDEO && video_threads > 0) { 217 codec_context->thread_count = video_threads; 218 } 219 220 // Initialize our codec. 221 if (avcodec_open2(codec_context, codec, NULL) < 0) { 222 std::cerr << "Error: Could not open codec " 223 << codec_context->codec->name << " for " 224 << in_path.value() << std::endl; 225 return 1; 226 } 227 228 // Buffer used for audio decoding. 229 scoped_ptr_malloc<AVFrame, media::ScopedPtrAVFree> audio_frame( 230 avcodec_alloc_frame()); 231 if (!audio_frame) { 232 std::cerr << "Error: avcodec_alloc_frame for " 233 << in_path.value() << std::endl; 234 return 1; 235 } 236 237 // Buffer used for video decoding. 238 scoped_ptr_malloc<AVFrame, media::ScopedPtrAVFree> video_frame( 239 avcodec_alloc_frame()); 240 if (!video_frame) { 241 std::cerr << "Error: avcodec_alloc_frame for " 242 << in_path.value() << std::endl; 243 return 1; 244 } 245 246 // Stats collector. 247 EnterTimingSection(); 248 std::vector<double> decode_times; 249 decode_times.reserve(4096); 250 // Parse through the entire stream until we hit EOF. 251#if SHOW_VERBOSE 252 base::TimeTicks start = base::TimeTicks::HighResNow(); 253#endif 254 int frames = 0; 255 int read_result = 0; 256 do { 257 read_result = av_read_frame(format_context, &packet); 258 259 if (read_result < 0) { 260 if (max_loops) { 261 --max_loops; 262 } 263 if (max_loops > 0) { 264 av_seek_frame(format_context, -1, 0, AVSEEK_FLAG_BACKWARD); 265 read_result = 0; 266 continue; 267 } 268 if (flush) { 269 packet.stream_index = target_stream; 270 packet.size = 0; 271 } else { 272 break; 273 } 274 } 275 276 // Only decode packets from our target stream. 277 if (packet.stream_index == target_stream) { 278 int result = -1; 279 if (target_codec == AVMEDIA_TYPE_AUDIO) { 280 int size_out = 0; 281 int got_audio = 0; 282 283 avcodec_get_frame_defaults(audio_frame.get()); 284 285 base::TimeTicks decode_start = base::TimeTicks::HighResNow(); 286 result = avcodec_decode_audio4(codec_context, audio_frame.get(), 287 &got_audio, &packet); 288 base::TimeDelta delta = base::TimeTicks::HighResNow() - decode_start; 289 290 if (got_audio) { 291 size_out = av_samples_get_buffer_size( 292 NULL, codec_context->channels, audio_frame->nb_samples, 293 codec_context->sample_fmt, 1); 294 } 295 296 if (got_audio && size_out) { 297 decode_times.push_back(delta.InMillisecondsF()); 298 ++frames; 299 read_result = 0; // Force continuation. 300 301 if (output) { 302 if (fwrite(audio_frame->data[0], 1, size_out, output) != 303 static_cast<size_t>(size_out)) { 304 std::cerr << "Error: Could not write " 305 << size_out << " bytes for " << in_path.value() 306 << std::endl; 307 return 1; 308 } 309 } 310 311 const uint8* u8_samples = 312 reinterpret_cast<const uint8*>(audio_frame->data[0]); 313 if (hash_djb2) { 314 hash_value = DJB2Hash(u8_samples, size_out, hash_value); 315 } 316 if (hash_md5) { 317 base::MD5Update( 318 &ctx, 319 base::StringPiece(reinterpret_cast<const char*>(u8_samples), 320 size_out)); 321 } 322 } 323 } else if (target_codec == AVMEDIA_TYPE_VIDEO) { 324 int got_picture = 0; 325 326 avcodec_get_frame_defaults(video_frame.get()); 327 328 base::TimeTicks decode_start = base::TimeTicks::HighResNow(); 329 result = avcodec_decode_video2(codec_context, video_frame.get(), 330 &got_picture, &packet); 331 base::TimeDelta delta = base::TimeTicks::HighResNow() - decode_start; 332 333 if (got_picture) { 334 decode_times.push_back(delta.InMillisecondsF()); 335 ++frames; 336 read_result = 0; // Force continuation. 337 338 for (int plane = 0; plane < 3; ++plane) { 339 const uint8* source = video_frame->data[plane]; 340 const size_t source_stride = video_frame->linesize[plane]; 341 size_t bytes_per_line = codec_context->width; 342 size_t copy_lines = codec_context->height; 343 if (plane != 0) { 344 switch (codec_context->pix_fmt) { 345 case PIX_FMT_YUV420P: 346 case PIX_FMT_YUVJ420P: 347 bytes_per_line /= 2; 348 copy_lines = (copy_lines + 1) / 2; 349 break; 350 case PIX_FMT_YUV422P: 351 case PIX_FMT_YUVJ422P: 352 bytes_per_line /= 2; 353 break; 354 case PIX_FMT_YUV444P: 355 case PIX_FMT_YUVJ444P: 356 break; 357 default: 358 std::cerr << "Error: Unknown video format " 359 << codec_context->pix_fmt; 360 return 1; 361 } 362 } 363 if (output) { 364 for (size_t i = 0; i < copy_lines; ++i) { 365 if (fwrite(source, 1, bytes_per_line, output) != 366 bytes_per_line) { 367 std::cerr << "Error: Could not write data after " 368 << copy_lines << " lines for " 369 << in_path.value() << std::endl; 370 return 1; 371 } 372 source += source_stride; 373 } 374 } 375 if (hash_djb2) { 376 for (size_t i = 0; i < copy_lines; ++i) { 377 hash_value = DJB2Hash(source, bytes_per_line, hash_value); 378 source += source_stride; 379 } 380 } 381 if (hash_md5) { 382 for (size_t i = 0; i < copy_lines; ++i) { 383 base::MD5Update( 384 &ctx, 385 base::StringPiece(reinterpret_cast<const char*>(source), 386 bytes_per_line)); 387 source += source_stride; 388 } 389 } 390 } 391 } 392 } else { 393 NOTREACHED(); 394 } 395 396 // Make sure our decoding went OK. 397 if (result < 0) { 398 std::cerr << "Error: avcodec_decode returned " 399 << result << " for " << in_path.value() << std::endl; 400 return 1; 401 } 402 } 403 // Free our packet. 404 av_free_packet(&packet); 405 406 if (max_frames && (frames >= max_frames)) 407 break; 408 } while (read_result >= 0); 409#if SHOW_VERBOSE 410 base::TimeDelta total = base::TimeTicks::HighResNow() - start; 411#endif 412 LeaveTimingSection(); 413 414 // Clean up. 415 if (output) 416 file_util::CloseFile(output); 417 418 // Calculate the sum of times. Note that some of these may be zero. 419 double sum = 0; 420 for (size_t i = 0; i < decode_times.size(); ++i) { 421 sum += decode_times[i]; 422 } 423 424 if (sum > 0) { 425 if (target_codec == AVMEDIA_TYPE_AUDIO) { 426 // Calculate the average milliseconds per frame. 427 // Audio decoding is usually in the millisecond or range, and 428 // best expressed in time (ms) rather than FPS, which can approach 429 // infinity. 430 double ms = sum / frames; 431 // Print our results. 432 log_out->setf(std::ios::fixed); 433 log_out->precision(2); 434 *log_out << "TIME PER FRAME (MS):" << std::setw(11) << ms << std::endl; 435 } else if (target_codec == AVMEDIA_TYPE_VIDEO) { 436 // Calculate the average frames per second. 437 // Video decoding is expressed in Frames Per Second - a term easily 438 // understood and should exceed a typical target of 30 fps. 439 double fps = frames * 1000.0 / sum; 440 // Print our results. 441 log_out->setf(std::ios::fixed); 442 log_out->precision(2); 443 *log_out << "FPS:" << std::setw(11) << fps << std::endl; 444 } 445 } 446 447#if SHOW_VERBOSE 448 // Print our results. 449 log_out->setf(std::ios::fixed); 450 log_out->precision(2); 451 *log_out << std::endl; 452 *log_out << " Frames:" << std::setw(11) << frames 453 << std::endl; 454 *log_out << " Total:" << std::setw(11) << total.InMillisecondsF() 455 << " ms" << std::endl; 456 *log_out << " Summation:" << std::setw(11) << sum 457 << " ms" << std::endl; 458 459 if (frames > 0) { 460 // Calculate the average time per frame. 461 double average = sum / frames; 462 463 // Calculate the sum of the squared differences. 464 // Standard deviation will only be accurate if no threads are used. 465 // TODO(fbarchard): Rethink standard deviation calculation. 466 double squared_sum = 0; 467 for (int i = 0; i < frames; ++i) { 468 double difference = decode_times[i] - average; 469 squared_sum += difference * difference; 470 } 471 472 // Calculate the standard deviation (jitter). 473 double stddev = sqrt(squared_sum / frames); 474 475 *log_out << " Average:" << std::setw(11) << average 476 << " ms" << std::endl; 477 *log_out << " StdDev:" << std::setw(11) << stddev 478 << " ms" << std::endl; 479 } 480 if (hash_djb2) { 481 *log_out << " DJB2 Hash:" << std::setw(11) << hash_value 482 << " " << in_path.value() << std::endl; 483 } 484 if (hash_md5) { 485 base::MD5Digest digest; // The result of the computation. 486 base::MD5Final(&digest, &ctx); 487 *log_out << " MD5 Hash: " << base::MD5DigestToBase16(digest) 488 << " " << in_path.value() << std::endl; 489 } 490#endif // SHOW_VERBOSE 491#if defined(ENABLE_WINDOWS_EXCEPTIONS) 492 } __except(EXCEPTION_EXECUTE_HANDLER) { 493 *log_out << " Exception:" << std::setw(11) << GetExceptionCode() 494 << " " << in_path.value() << std::endl; 495 return 1; 496 } 497#endif 498 CommandLine::Reset(); 499 return 0; 500} 501