1/*
2 *  Copyright (c) 2012 The WebRTC project authors. All Rights Reserved.
3 *
4 *  Use of this source code is governed by a BSD-style license
5 *  that can be found in the LICENSE file in the root of the source
6 *  tree. An additional intellectual property rights grant can be found
7 *  in the file PATENTS.  All contributing project authors may
8 *  be found in the AUTHORS file in the root of the source tree.
9 */
10
11#include <assert.h>
12#include <stdio.h>
13#include <time.h>
14
15#include <stdarg.h>
16#include <sys/stat.h>  // To check for directory existence.
17
18#ifndef S_ISDIR  // Not defined in stat.h on Windows.
19#define S_ISDIR(mode) (((mode) & S_IFMT) == S_IFDIR)
20#endif
21
22#include "gflags/gflags.h"
23#include "webrtc/common_types.h"
24#include "webrtc/modules/video_coding/codecs/test/packet_manipulator.h"
25#include "webrtc/modules/video_coding/codecs/test/stats.h"
26#include "webrtc/modules/video_coding/codecs/test/videoprocessor.h"
27#include "webrtc/modules/video_coding/codecs/vp8/include/vp8.h"
28#include "webrtc/modules/video_coding/main/interface/video_coding.h"
29#include "webrtc/system_wrappers/interface/trace.h"
30#include "webrtc/test/testsupport/frame_reader.h"
31#include "webrtc/test/testsupport/frame_writer.h"
32#include "webrtc/test/testsupport/metrics/video_metrics.h"
33#include "webrtc/test/testsupport/packet_reader.h"
34
35DEFINE_string(test_name, "Quality test", "The name of the test to run. ");
36DEFINE_string(test_description, "", "A more detailed description about what "
37              "the current test is about.");
38DEFINE_string(input_filename, "", "Input file. "
39              "The source video file to be encoded and decoded. Must be in "
40              ".yuv format");
41DEFINE_int32(width, -1, "Width in pixels of the frames in the input file.");
42DEFINE_int32(height, -1, "Height in pixels of the frames in the input file.");
43DEFINE_int32(framerate, 30, "Frame rate of the input file, in FPS "
44             "(frames-per-second). ");
45DEFINE_string(output_dir, ".", "Output directory. "
46              "The directory where the output file will be put. Must already "
47              "exist.");
48DEFINE_bool(use_single_core, false, "Force using a single core. If set to "
49            "true, only one core will be used for processing. Using a single "
50            "core is necessary to get a deterministic behavior for the"
51            "encoded frames - using multiple cores will produce different "
52            "encoded frames since multiple cores are competing to consume the "
53            "byte budget for each frame in parallel. If set to false, "
54            "the maximum detected number of cores will be used. ");
55DEFINE_bool(disable_fixed_random_seed , false, "Set this flag to disable the"
56            "usage of a fixed random seed for the random generator used "
57            "for packet loss. Disabling this will cause consecutive runs "
58            "loose packets at different locations, which is bad for "
59            "reproducibility.");
60DEFINE_string(output_filename, "", "Output file. "
61              "The name of the output video file resulting of the processing "
62              "of the source file. By default this is the same name as the "
63              "input file with '_out' appended before the extension.");
64DEFINE_int32(bitrate, 500, "Bit rate in kilobits/second.");
65DEFINE_int32(keyframe_interval, 0, "Forces a keyframe every Nth frame. "
66             "0 means the encoder decides when to insert keyframes.  Note that "
67             "the encoder may create a keyframe in other locations in addition "
68             "to the interval that is set using this parameter.");
69DEFINE_int32(temporal_layers, 0, "The number of temporal layers to use "
70             "(VP8 specific codec setting). Must be 0-4.");
71DEFINE_int32(packet_size, 1500, "Simulated network packet size in bytes (MTU). "
72             "Used for packet loss simulation.");
73DEFINE_int32(max_payload_size, 1440, "Max payload size in bytes for the "
74             "encoder.");
75DEFINE_string(packet_loss_mode, "uniform", "Packet loss mode. Two different "
76              "packet loss models are supported: uniform or burst. This "
77              "setting has no effect unless packet_loss_rate is >0. ");
78DEFINE_double(packet_loss_probability, 0.0, "Packet loss probability. A value "
79              "between 0.0 and 1.0 that defines the probability of a packet "
80              "being lost. 0.1 means 10% and so on.");
81DEFINE_int32(packet_loss_burst_length, 1, "Packet loss burst length. Defines "
82             "how many packets will be lost in a burst when a packet has been "
83             "decided to be lost. Must be >=1.");
84DEFINE_bool(csv, false, "CSV output. Enabling this will output all frame "
85            "statistics at the end of execution. Recommended to run combined "
86            "with --noverbose to avoid mixing output.");
87DEFINE_bool(python, false, "Python output. Enabling this will output all frame "
88            "statistics as a Python script at the end of execution. "
89            "Recommended to run combine with --noverbose to avoid mixing "
90            "output.");
91DEFINE_bool(verbose, true, "Verbose mode. Prints a lot of debugging info. "
92            "Suitable for tracking progress but not for capturing output. "
93            "Disable with --noverbose flag.");
94
95// Custom log method that only prints if the verbose flag is given.
96// Supports all the standard printf parameters and formatting (just forwarded).
97int Log(const char *format, ...) {
98  int result = 0;
99  if (FLAGS_verbose) {
100    va_list args;
101    va_start(args, format);
102    result = vprintf(format, args);
103    va_end(args);
104  }
105  return result;
106}
107
108// Validates the arguments given as command line flags and fills in the
109// TestConfig struct with all configurations needed for video processing.
110// Returns 0 if everything is OK, otherwise an exit code.
111int HandleCommandLineFlags(webrtc::test::TestConfig* config) {
112  // Validate the mandatory flags:
113  if (FLAGS_input_filename == "" || FLAGS_width == -1 || FLAGS_height == -1) {
114    printf("%s\n", google::ProgramUsage());
115    return 1;
116  }
117  config->name = FLAGS_test_name;
118  config->description = FLAGS_test_description;
119
120  // Verify the input file exists and is readable.
121  FILE* test_file;
122  test_file = fopen(FLAGS_input_filename.c_str(), "rb");
123  if (test_file == NULL) {
124    fprintf(stderr, "Cannot read the specified input file: %s\n",
125            FLAGS_input_filename.c_str());
126    return 2;
127  }
128  fclose(test_file);
129  config->input_filename = FLAGS_input_filename;
130
131  // Verify the output dir exists.
132  struct stat dir_info;
133  if (!(stat(FLAGS_output_dir.c_str(), &dir_info) == 0 &&
134      S_ISDIR(dir_info.st_mode))) {
135    fprintf(stderr, "Cannot find output directory: %s\n",
136              FLAGS_output_dir.c_str());
137    return 3;
138  }
139  config->output_dir = FLAGS_output_dir;
140
141  // Manufacture an output filename if none was given.
142  if (FLAGS_output_filename == "") {
143    // Cut out the filename without extension from the given input file
144    // (which may include a path)
145    int startIndex = FLAGS_input_filename.find_last_of("/") + 1;
146    if (startIndex == 0) {
147      startIndex = 0;
148    }
149    FLAGS_output_filename =
150        FLAGS_input_filename.substr(startIndex,
151                                    FLAGS_input_filename.find_last_of(".")
152                                    - startIndex) + "_out.yuv";
153  }
154
155  // Verify output file can be written.
156  if (FLAGS_output_dir == ".") {
157    config->output_filename = FLAGS_output_filename;
158  } else {
159    config->output_filename = FLAGS_output_dir + "/"+ FLAGS_output_filename;
160  }
161  test_file = fopen(config->output_filename.c_str(), "wb");
162  if (test_file == NULL) {
163    fprintf(stderr, "Cannot write output file: %s\n",
164            config->output_filename.c_str());
165    return 4;
166  }
167  fclose(test_file);
168
169  // Check single core flag.
170  config->use_single_core = FLAGS_use_single_core;
171
172  // Get codec specific configuration.
173  webrtc::VideoCodingModule::Codec(webrtc::kVideoCodecVP8,
174                                   config->codec_settings);
175
176  // Check the temporal layers.
177  if (FLAGS_temporal_layers < 0 ||
178      FLAGS_temporal_layers > webrtc::kMaxTemporalStreams) {
179    fprintf(stderr, "Temporal layers number must be 0-4, was: %d\n",
180            FLAGS_temporal_layers);
181    return 13;
182  }
183  config->codec_settings->codecSpecific.VP8.numberOfTemporalLayers =
184      FLAGS_temporal_layers;
185
186  // Check the bit rate.
187  if (FLAGS_bitrate <= 0) {
188    fprintf(stderr, "Bit rate must be >0 kbps, was: %d\n", FLAGS_bitrate);
189    return 5;
190  }
191  config->codec_settings->startBitrate = FLAGS_bitrate;
192
193  // Check the keyframe interval.
194  if (FLAGS_keyframe_interval < 0) {
195    fprintf(stderr, "Keyframe interval must be >=0, was: %d\n",
196            FLAGS_keyframe_interval);
197    return 6;
198  }
199  config->keyframe_interval = FLAGS_keyframe_interval;
200
201  // Check packet size and max payload size.
202  if (FLAGS_packet_size <= 0) {
203    fprintf(stderr, "Packet size must be >0 bytes, was: %d\n",
204            FLAGS_packet_size);
205    return 7;
206  }
207  config->networking_config.packet_size_in_bytes = FLAGS_packet_size;
208
209  if (FLAGS_max_payload_size <= 0) {
210    fprintf(stderr, "Max payload size must be >0 bytes, was: %d\n",
211            FLAGS_max_payload_size);
212    return 8;
213  }
214  config->networking_config.max_payload_size_in_bytes =
215      FLAGS_max_payload_size;
216
217  // Check the width and height
218  if (FLAGS_width <= 0 || FLAGS_height <= 0) {
219    fprintf(stderr, "Width and height must be >0.");
220    return 9;
221  }
222  config->codec_settings->width = FLAGS_width;
223  config->codec_settings->height = FLAGS_height;
224  config->codec_settings->maxFramerate = FLAGS_framerate;
225
226  // Calculate the size of each frame to read (according to YUV spec).
227  config->frame_length_in_bytes =
228      3 * config->codec_settings->width * config->codec_settings->height / 2;
229
230  // Check packet loss settings
231  if (FLAGS_packet_loss_mode != "uniform" &&
232      FLAGS_packet_loss_mode != "burst") {
233    fprintf(stderr, "Unsupported packet loss mode, must be 'uniform' or "
234            "'burst'\n.");
235    return 10;
236  }
237  config->networking_config.packet_loss_mode = webrtc::test::kUniform;
238  if (FLAGS_packet_loss_mode == "burst") {
239    config->networking_config.packet_loss_mode =  webrtc::test::kBurst;
240  }
241
242  if (FLAGS_packet_loss_probability < 0.0 ||
243      FLAGS_packet_loss_probability > 1.0) {
244    fprintf(stderr, "Invalid packet loss probability. Must be 0.0 - 1.0, "
245            "was: %f\n", FLAGS_packet_loss_probability);
246    return 11;
247  }
248  config->networking_config.packet_loss_probability =
249      FLAGS_packet_loss_probability;
250
251  if (FLAGS_packet_loss_burst_length < 1) {
252    fprintf(stderr, "Invalid packet loss burst length, must be >=1, "
253            "was: %d\n", FLAGS_packet_loss_burst_length);
254    return 12;
255  }
256  config->networking_config.packet_loss_burst_length =
257      FLAGS_packet_loss_burst_length;
258  config->verbose = FLAGS_verbose;
259  return 0;
260}
261
262void CalculateSsimVideoMetrics(webrtc::test::TestConfig* config,
263                               webrtc::test::QualityMetricsResult* result) {
264  Log("Calculating SSIM...\n");
265  I420SSIMFromFiles(config->input_filename.c_str(),
266                    config->output_filename.c_str(),
267                    config->codec_settings->width,
268                    config->codec_settings->height, result);
269  Log("  Average: %3.2f\n", result->average);
270  Log("  Min    : %3.2f (frame %d)\n", result->min, result->min_frame_number);
271  Log("  Max    : %3.2f (frame %d)\n", result->max, result->max_frame_number);
272}
273
274void CalculatePsnrVideoMetrics(webrtc::test::TestConfig* config,
275                               webrtc::test::QualityMetricsResult* result) {
276  Log("Calculating PSNR...\n");
277  I420PSNRFromFiles(config->input_filename.c_str(),
278                    config->output_filename.c_str(),
279                    config->codec_settings->width,
280                    config->codec_settings->height, result);
281  Log("  Average: %3.2f\n", result->average);
282  Log("  Min    : %3.2f (frame %d)\n", result->min, result->min_frame_number);
283  Log("  Max    : %3.2f (frame %d)\n", result->max, result->max_frame_number);
284}
285
286void PrintConfigurationSummary(const webrtc::test::TestConfig& config) {
287  Log("Quality test with parameters:\n");
288  Log("  Test name        : %s\n", config.name.c_str());
289  Log("  Description      : %s\n", config.description.c_str());
290  Log("  Input filename   : %s\n", config.input_filename.c_str());
291  Log("  Output directory : %s\n", config.output_dir.c_str());
292  Log("  Output filename  : %s\n", config.output_filename.c_str());
293  Log("  Frame length       : %d bytes\n", config.frame_length_in_bytes);
294  Log("  Packet size      : %d bytes\n",
295      config.networking_config.packet_size_in_bytes);
296  Log("  Max payload size : %d bytes\n",
297      config.networking_config.max_payload_size_in_bytes);
298  Log("  Packet loss:\n");
299  Log("    Mode           : %s\n",
300      PacketLossModeToStr(config.networking_config.packet_loss_mode));
301  Log("    Probability    : %2.1f\n",
302      config.networking_config.packet_loss_probability);
303  Log("    Burst length   : %d packets\n",
304      config.networking_config.packet_loss_burst_length);
305}
306
307void PrintCsvOutput(const webrtc::test::Stats& stats,
308                    const webrtc::test::QualityMetricsResult& ssim_result,
309                    const webrtc::test::QualityMetricsResult& psnr_result) {
310  Log("\nCSV output (recommended to run with --noverbose to skip the "
311              "above output)\n");
312  printf("frame_number encoding_successful decoding_successful "
313      "encode_return_code decode_return_code "
314      "encode_time_in_us decode_time_in_us "
315      "bit_rate_in_kbps encoded_frame_length_in_bytes frame_type "
316      "packets_dropped total_packets "
317      "ssim psnr\n");
318
319  for (unsigned int i = 0; i < stats.stats_.size(); ++i) {
320    const webrtc::test::FrameStatistic& f = stats.stats_[i];
321    const webrtc::test::FrameResult& ssim = ssim_result.frames[i];
322    const webrtc::test::FrameResult& psnr = psnr_result.frames[i];
323    printf("%4d, %d, %d, %2d, %2d, %6d, %6d, %5d, %7d, %d, %2d, %2d, "
324           "%5.3f, %5.2f\n",
325           f.frame_number,
326           f.encoding_successful,
327           f.decoding_successful,
328           f.encode_return_code,
329           f.decode_return_code,
330           f.encode_time_in_us,
331           f.decode_time_in_us,
332           f.bit_rate_in_kbps,
333           f.encoded_frame_length_in_bytes,
334           f.frame_type,
335           f.packets_dropped,
336           f.total_packets,
337           ssim.value,
338           psnr.value);
339  }
340}
341
342void PrintPythonOutput(const webrtc::test::TestConfig& config,
343                       const webrtc::test::Stats& stats,
344                       const webrtc::test::QualityMetricsResult& ssim_result,
345                       const webrtc::test::QualityMetricsResult& psnr_result) {
346  Log("\nPython output (recommended to run with --noverbose to skip the "
347               "above output)\n");
348  printf("test_configuration = ["
349         "{'name': 'name',                      'value': '%s'},\n"
350         "{'name': 'description',               'value': '%s'},\n"
351         "{'name': 'test_number',               'value': '%d'},\n"
352         "{'name': 'input_filename',            'value': '%s'},\n"
353         "{'name': 'output_filename',           'value': '%s'},\n"
354         "{'name': 'output_dir',                'value': '%s'},\n"
355         "{'name': 'packet_size_in_bytes',      'value': '%d'},\n"
356         "{'name': 'max_payload_size_in_bytes', 'value': '%d'},\n"
357         "{'name': 'packet_loss_mode',          'value': '%s'},\n"
358         "{'name': 'packet_loss_probability',   'value': '%f'},\n"
359         "{'name': 'packet_loss_burst_length',  'value': '%d'},\n"
360         "{'name': 'exclude_frame_types',       'value': '%s'},\n"
361         "{'name': 'frame_length_in_bytes',     'value': '%d'},\n"
362         "{'name': 'use_single_core',           'value': '%s'},\n"
363         "{'name': 'keyframe_interval;',        'value': '%d'},\n"
364         "{'name': 'video_codec_type',          'value': '%s'},\n"
365         "{'name': 'width',                     'value': '%d'},\n"
366         "{'name': 'height',                    'value': '%d'},\n"
367         "{'name': 'bit_rate_in_kbps',          'value': '%d'},\n"
368         "]\n",
369         config.name.c_str(),
370         config.description.c_str(),
371         config.test_number,
372         config.input_filename.c_str(),
373         config.output_filename.c_str(),
374         config.output_dir.c_str(),
375         config.networking_config.packet_size_in_bytes,
376         config.networking_config.max_payload_size_in_bytes,
377         PacketLossModeToStr(config.networking_config.packet_loss_mode),
378         config.networking_config.packet_loss_probability,
379         config.networking_config.packet_loss_burst_length,
380         ExcludeFrameTypesToStr(config.exclude_frame_types),
381         config.frame_length_in_bytes,
382         config.use_single_core ? "True " : "False",
383         config.keyframe_interval,
384         webrtc::test::VideoCodecTypeToStr(config.codec_settings->codecType),
385         config.codec_settings->width,
386         config.codec_settings->height,
387         config.codec_settings->startBitrate);
388  printf("frame_data_types = {"
389         "'frame_number': ('number', 'Frame number'),\n"
390         "'encoding_successful': ('boolean', 'Encoding successful?'),\n"
391         "'decoding_successful': ('boolean', 'Decoding successful?'),\n"
392         "'encode_time': ('number', 'Encode time (us)'),\n"
393         "'decode_time': ('number', 'Decode time (us)'),\n"
394         "'encode_return_code': ('number', 'Encode return code'),\n"
395         "'decode_return_code': ('number', 'Decode return code'),\n"
396         "'bit_rate': ('number', 'Bit rate (kbps)'),\n"
397         "'encoded_frame_length': "
398         "('number', 'Encoded frame length (bytes)'),\n"
399         "'frame_type': ('string', 'Frame type'),\n"
400         "'packets_dropped': ('number', 'Packets dropped'),\n"
401         "'total_packets': ('number', 'Total packets'),\n"
402         "'ssim': ('number', 'SSIM'),\n"
403         "'psnr': ('number', 'PSNR (dB)'),\n"
404         "}\n");
405  printf("frame_data = [");
406  for (unsigned int i = 0; i < stats.stats_.size(); ++i) {
407    const webrtc::test::FrameStatistic& f = stats.stats_[i];
408    const webrtc::test::FrameResult& ssim = ssim_result.frames[i];
409    const webrtc::test::FrameResult& psnr = psnr_result.frames[i];
410    printf("{'frame_number': %d, "
411           "'encoding_successful': %s, 'decoding_successful': %s, "
412           "'encode_time': %d, 'decode_time': %d, "
413           "'encode_return_code': %d, 'decode_return_code': %d, "
414           "'bit_rate': %d, 'encoded_frame_length': %d, 'frame_type': %s, "
415           "'packets_dropped': %d, 'total_packets': %d, "
416           "'ssim': %f, 'psnr': %f},\n",
417           f.frame_number,
418           f.encoding_successful ? "True " : "False",
419           f.decoding_successful ? "True " : "False",
420           f.encode_time_in_us,
421           f.decode_time_in_us,
422           f.encode_return_code,
423           f.decode_return_code,
424           f.bit_rate_in_kbps,
425           f.encoded_frame_length_in_bytes,
426           f.frame_type == webrtc::kDeltaFrame ? "'Delta'" : "'Other'",
427           f.packets_dropped,
428           f.total_packets,
429           ssim.value,
430           psnr.value);
431  }
432  printf("]\n");
433}
434
435// Runs a quality measurement on the input file supplied to the program.
436// The input file must be in YUV format.
437int main(int argc, char* argv[]) {
438  std::string program_name = argv[0];
439  std::string usage = "Quality test application for video comparisons.\n"
440    "Run " + program_name + " --helpshort for usage.\n"
441    "Example usage:\n" + program_name +
442    " --input_filename=filename.yuv --width=352 --height=288\n";
443  google::SetUsageMessage(usage);
444
445  google::ParseCommandLineFlags(&argc, &argv, true);
446
447  // Create TestConfig and codec settings struct.
448  webrtc::test::TestConfig config;
449  webrtc::VideoCodec codec_settings;
450  config.codec_settings = &codec_settings;
451
452  int return_code = HandleCommandLineFlags(&config);
453  // Exit if an invalid argument is supplied.
454  if (return_code != 0) {
455    return return_code;
456  }
457
458  PrintConfigurationSummary(config);
459
460  webrtc::VP8Encoder* encoder = webrtc::VP8Encoder::Create();
461  webrtc::VP8Decoder* decoder = webrtc::VP8Decoder::Create();
462  webrtc::test::Stats stats;
463  webrtc::test::FrameReaderImpl frame_reader(config.input_filename,
464                                             config.frame_length_in_bytes);
465  webrtc::test::FrameWriterImpl frame_writer(config.output_filename,
466                                             config.frame_length_in_bytes);
467  frame_reader.Init();
468  frame_writer.Init();
469  webrtc::test::PacketReader packet_reader;
470
471  webrtc::test::PacketManipulatorImpl packet_manipulator(
472      &packet_reader, config.networking_config, config.verbose);
473  // By default the packet manipulator is seeded with a fixed random.
474  // If disabled we must generate a new seed.
475  if (FLAGS_disable_fixed_random_seed) {
476    packet_manipulator.InitializeRandomSeed(time(NULL));
477  }
478  webrtc::test::VideoProcessor* processor =
479      new webrtc::test::VideoProcessorImpl(encoder, decoder,
480                                           &frame_reader,
481                                           &frame_writer,
482                                           &packet_manipulator,
483                                           config, &stats);
484  processor->Init();
485
486  int frame_number = 0;
487  while (processor->ProcessFrame(frame_number)) {
488    if (frame_number % 80 == 0) {
489      Log("\n");  // make the output a bit nicer.
490    }
491    Log(".");
492    frame_number++;
493  }
494  Log("\n");
495  Log("Processed %d frames\n", frame_number);
496
497  // Release encoder and decoder to make sure they have finished processing.
498  encoder->Release();
499  decoder->Release();
500
501  // Verify statistics are correct:
502  assert(frame_number == static_cast<int>(stats.stats_.size()));
503
504  // Close the files before we start using them for SSIM/PSNR calculations.
505  frame_reader.Close();
506  frame_writer.Close();
507
508  stats.PrintSummary();
509
510  webrtc::test::QualityMetricsResult ssim_result;
511  CalculateSsimVideoMetrics(&config, &ssim_result);
512  webrtc::test::QualityMetricsResult psnr_result;
513  CalculatePsnrVideoMetrics(&config, &psnr_result);
514
515  if (FLAGS_csv) {
516    PrintCsvOutput(stats, ssim_result, psnr_result);
517  }
518  if (FLAGS_python) {
519    PrintPythonOutput(config, stats, ssim_result, psnr_result);
520  }
521  delete processor;
522  delete encoder;
523  delete decoder;
524  Log("Quality test finished!");
525  return 0;
526}
527