Add --step for temporal subsampling

If --step is set to a value N, then every Nth frame will
be encoded.

The input stream "I" should be treated as having the following
filters applied to it, in this order:

1. I is fed to the skip filter. If --skip is set, the first N frames are
  dropped, producing a new stream I'.

2. I' is fed to the step filter. If --step is set, every Nth frame is
  output into a new stream I''.

3. I'' is fed through the limit filter. If --limit is set, then
  this filter lets through the first N frames and drops the rest. This
  final stream is sent to the encoder.

Examples (assuming the first frame is "frame 1"):

--limit=5 --skip=0 --step=1
  Frames 1 through 5 are encoded.

--limit=5 --skip=5 --step=1
  Frames 6 through 10 are encoded.

--limit=5 --skip=5 --step=2
  Frames 6, 8, 10, 12, and 14 are encoded.

Change-Id: I08448a1e720904ae4b1cf96e99e8e6ec6678ce09
diff --git a/CMakeLists.txt b/CMakeLists.txt
index ce84d45..9be8f4d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -155,6 +155,7 @@
             "${AOM_ROOT}/common/warnings.h"
             "${AOM_ROOT}/common/y4minput.c"
             "${AOM_ROOT}/common/y4minput.h"
+            "${AOM_ROOT}/common/stream_iter.c"
             "${AOM_ROOT}/examples/encoder_util.h"
             "${AOM_ROOT}/examples/encoder_util.c")
 
diff --git a/apps/aomenc.c b/apps/aomenc.c
index 3ec6bdc..2a05757 100644
--- a/apps/aomenc.c
+++ b/apps/aomenc.c
@@ -15,6 +15,7 @@
 #include <limits.h>
 #include <math.h>
 #include <stdarg.h>
+#include <stdbool.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
@@ -32,6 +33,7 @@
 #include "aom_ports/mem_ops.h"
 #include "common/args.h"
 #include "common/ivfenc.h"
+#include "common/stream_iter.h"
 #include "common/tools_common.h"
 #include "common/warnings.h"
 
@@ -103,20 +105,6 @@
   va_end(ap);
 }
 
-static int read_frame(struct AvxInputContext *input_ctx, aom_image_t *img) {
-  FILE *f = input_ctx->file;
-  y4m_input *y4m = &input_ctx->y4m;
-  int shortread = 0;
-
-  if (input_ctx->file_type == FILE_TYPE_Y4M) {
-    if (y4m_input_fetch_frame(y4m, f, img) < 1) return 0;
-  } else {
-    shortread = read_yuv_frame(input_ctx, img);
-  }
-
-  return !shortread;
-}
-
 static int file_is_y4m(const char detect[4]) {
   if (memcmp(detect, "YUV4", 4) == 0) {
     return 1;
@@ -156,6 +144,8 @@
     ARG_DEF(NULL, "limit", 1, "Stop encoding after n input frames");
 static const arg_def_t skip =
     ARG_DEF(NULL, "skip", 1, "Skip the first n input frames");
+static const arg_def_t step = ARG_DEF(
+    NULL, "step", 1, "Encode every n-th frame (after the --skip frames)");
 static const arg_def_t good_dl =
     ARG_DEF(NULL, "good", 0, "Use Good Quality Deadline");
 static const arg_def_t rt_dl =
@@ -218,6 +208,7 @@
                                         &fpf_name,
                                         &limit,
                                         &skip,
+                                        &step,
                                         &good_dl,
                                         &rt_dl,
                                         &quietarg,
@@ -1195,6 +1186,7 @@
   global->passes = 0;
   global->color_type = I420;
   global->csp = AOM_CSP_UNKNOWN;
+  global->step_frames = 1;
 
   int cfg_included = 0;
   init_config(&global->encoder_config);
@@ -1261,7 +1253,12 @@
       global->limit = arg_parse_uint(&arg);
     else if (arg_match(&arg, &skip, argi))
       global->skip_frames = arg_parse_uint(&arg);
-    else if (arg_match(&arg, &psnrarg, argi))
+    else if (arg_match(&arg, &step, argi)) {
+      global->step_frames = arg_parse_uint(&arg);
+      if (global->step_frames == 0) {
+        die("--step must be positive");
+      }
+    } else if (arg_match(&arg, &psnrarg, argi))
       global->show_psnr = 1;
     else if (arg_match(&arg, &recontest, argi))
       global->test_decode = arg_parse_enum_or_int(&arg);
@@ -1822,6 +1819,25 @@
   }
 }
 
+static void print_frames_to_code(FILE *f, struct stream_state *stream,
+                                 struct AvxEncoderConfig *global) {
+  const struct aom_codec_enc_cfg *cfg = &stream->config.cfg;
+  int num_frames = cfg->g_limit / global->step_frames;
+  // E.g., if step == 3 and limit == 4, then 2 frames are encoded.
+  // Similar for all cases where limit % step != 0.
+  if (cfg->g_limit % global->step_frames != 0) {
+    ++num_frames;
+  }
+  fprintf(f, "Frames to be coded             : %d - %d (%d frames",
+          global->skip_frames, global->skip_frames + cfg->g_limit - 1,
+          num_frames);
+  if (global->step_frames == 1) {
+    fprintf(f, ")\n");
+  } else {
+    fprintf(f, ", step size:%d)\n", global->step_frames);
+  }
+}
+
 static void show_stream_config(struct stream_state *stream,
                                struct AvxEncoderConfig *global,
                                struct AvxInputContext *input) {
@@ -1840,9 +1856,7 @@
           (double)global->framerate.num / (double)global->framerate.den,
           input->bit_depth);
   fprintf(stdout, "Number of threads              : %d\n", cfg->g_threads);
-  fprintf(stdout, "Frames to be coded             : %d - %d (%d frames)\n",
-          global->skip_frames, global->skip_frames + cfg->g_limit - 1,
-          cfg->g_limit);
+  print_frames_to_code(stdout, stream, global);
   fprintf(stdout, "Operating bit depth            : %d\n", cfg->g_bit_depth);
   fprintf(stdout, "Num of coding passes           : %d\n", global->passes);
 #if !CONFIG_SINGLEPASS
@@ -2366,7 +2380,6 @@
   int allocated_raw_shift = 0;
   int do_16bit_internal = 0;
   int input_shift = 0;
-  int frame_avail, got_data;
 
   struct AvxInputContext input;
   struct AvxEncoderConfig global;
@@ -2443,10 +2456,6 @@
     input.only_i420 = 0;
 
   for (pass = global.pass ? global.pass - 1 : 0; pass < global.passes; pass++) {
-    int frames_in = 0, seen_frames = 0;
-    int64_t average_rate = -1;
-    int64_t lagged_count = 0;
-
     open_input_file(&input, global.csp);
 
     /* If the input file doesn't specify its w/h (raw files), try to get
@@ -2698,92 +2707,85 @@
       };
     }
 
-    frame_avail = 1;
-    got_data = 0;
+    // Keep track of the total number of frames passed to the encoder.
+    int seen_frames = 0;
+    // Does the encoder have queued data that needs retrieval?
+    int got_data = 0;
+    // Is there a frame available for processing?
+    int frame_avail = 1;
 
+    // Wrap the original stream of frames in an "object" that returns the
+    // same set of streams.
+    StreamIter orig_stream;
+    copy_stream_iter_init(&orig_stream, &input);
+
+    // The skip iterator will skip the first N frames. Wrap the original
+    // stream in the skip iterator.
+    StreamIter skip_stream;
+    skip_stream_iter_init(&skip_stream, &orig_stream, global.skip_frames);
+
+    // The step iterator will only return every N-th frame.
+    StreamIter step_stream;
+    step_stream_iter_init(&step_stream, &skip_stream, global.step_frames);
+
+    // The limit iterator will stop returning frames after the N-th.
+    StreamIter limit_stream;
+    limit_stream_iter_init(&limit_stream, &step_stream, global.limit);
     while (frame_avail || got_data) {
-      struct aom_usec_timer timer;
+      frame_avail = read_stream_iter(&limit_stream, &raw);
+      if (frame_avail) {
+        seen_frames++;
+      }
+      fflush(stdout);
 
-      if (!global.limit || frames_in < global.limit) {
-        frame_avail = read_frame(&input, &raw);
-
-        if (frame_avail) frames_in++;
-        seen_frames =
-            frames_in > global.skip_frames ? frames_in - global.skip_frames : 0;
+      aom_image_t *frame_to_encode;
+      if (input_shift || (do_16bit_internal && input.bit_depth == 8)) {
+        assert(do_16bit_internal);
+        // Input bit depth and stream bit depth do not match, so up
+        // shift frame to stream bit depth
+        if (!allocated_raw_shift) {
+          aom_img_alloc(&raw_shift, raw.fmt | AOM_IMG_FMT_HIGHBITDEPTH,
+                        input.width, input.height, 32);
+          allocated_raw_shift = 1;
+        }
+        aom_img_upshift(&raw_shift, &raw, input_shift);
+        frame_to_encode = &raw_shift;
       } else {
-        frame_avail = 0;
+        frame_to_encode = &raw;
       }
-
-      if (frames_in > global.skip_frames) {
-        aom_image_t *frame_to_encode;
-        if (input_shift || (do_16bit_internal && input.bit_depth == 8)) {
-          assert(do_16bit_internal);
-          // Input bit depth and stream bit depth do not match, so up
-          // shift frame to stream bit depth
-          if (!allocated_raw_shift) {
-            aom_img_alloc(&raw_shift, raw.fmt | AOM_IMG_FMT_HIGHBITDEPTH,
-                          input.width, input.height, 32);
-            allocated_raw_shift = 1;
-          }
-          aom_img_upshift(&raw_shift, &raw, input_shift);
-          frame_to_encode = &raw_shift;
-        } else {
-          frame_to_encode = &raw;
-        }
-        aom_usec_timer_start(&timer);
-        if (do_16bit_internal) {
-          assert(frame_to_encode->fmt & AOM_IMG_FMT_HIGHBITDEPTH);
-          FOREACH_STREAM(stream, streams) {
-            if (stream->config.use_16bit_internal)
-              encode_frame(stream, &global,
-                           frame_avail ? frame_to_encode : NULL, frames_in);
-            else
-              assert(0);
-          };
-        } else {
-          assert((frame_to_encode->fmt & AOM_IMG_FMT_HIGHBITDEPTH) == 0);
-          FOREACH_STREAM(stream, streams) {
-            encode_frame(stream, &global, frame_avail ? frame_to_encode : NULL,
-                         frames_in);
-          }
-        }
-        aom_usec_timer_mark(&timer);
-        cx_time += aom_usec_timer_elapsed(&timer);
-
-        FOREACH_STREAM(stream, streams) { update_quantizer_histogram(stream); }
-
-        got_data = 0;
+      struct aom_usec_timer timer;
+      aom_usec_timer_start(&timer);
+      if (do_16bit_internal) {
+        assert(frame_to_encode->fmt & AOM_IMG_FMT_HIGHBITDEPTH);
         FOREACH_STREAM(stream, streams) {
-          get_cx_data(stream, &global, &got_data);
-        }
-
-        if (!got_data && input.length && streams != NULL &&
-            !streams->frames_out) {
-          lagged_count = global.limit ? seen_frames : ftello(input.file);
-        } else if (input.length) {
-          int64_t rate;
-
-          if (global.limit) {
-            const int64_t frame_in_lagged = (seen_frames - lagged_count) * 1000;
-
-            rate = cx_time ? frame_in_lagged * (int64_t)1000000 / cx_time : 0;
-          } else {
-            const int64_t input_pos = ftello(input.file);
-            const int64_t input_pos_lagged = input_pos - lagged_count;
-            rate = cx_time ? input_pos_lagged * (int64_t)1000000 / cx_time : 0;
-          }
-
-          average_rate =
-              (average_rate <= 0) ? rate : (average_rate * 7 + rate) / 8;
-        }
-
-        if (got_data && global.test_decode != TEST_DECODE_OFF) {
-          FOREACH_STREAM(stream, streams) {
-            test_decode(stream, global.test_decode);
-          }
+          if (stream->config.use_16bit_internal)
+            encode_frame(stream, &global, frame_avail ? frame_to_encode : NULL,
+                         seen_frames);
+          else
+            assert(0);
+        };
+      } else {
+        assert((frame_to_encode->fmt & AOM_IMG_FMT_HIGHBITDEPTH) == 0);
+        FOREACH_STREAM(stream, streams) {
+          encode_frame(stream, &global, frame_avail ? frame_to_encode : NULL,
+                       seen_frames);
         }
       }
+      aom_usec_timer_mark(&timer);
+      cx_time += aom_usec_timer_elapsed(&timer);
 
+      FOREACH_STREAM(stream, streams) { update_quantizer_histogram(stream); }
+
+      got_data = 0;
+      FOREACH_STREAM(stream, streams) {
+        get_cx_data(stream, &global, &got_data);
+      }
+
+      if (got_data && global.test_decode != TEST_DECODE_OFF) {
+        FOREACH_STREAM(stream, streams) {
+          test_decode(stream, global.test_decode);
+        }
+      }
       fflush(stdout);
     }
 
diff --git a/apps/aomenc.h b/apps/aomenc.h
index ceaf954..b0982db 100644
--- a/apps/aomenc.h
+++ b/apps/aomenc.h
@@ -43,6 +43,7 @@
   int verbose;
   int limit;
   int skip_frames;
+  int step_frames;
   int show_psnr;
   enum TestDecodeFatality test_decode;
   int have_framerate;
diff --git a/common/stream_iter.c b/common/stream_iter.c
new file mode 100644
index 0000000..bf523d8
--- /dev/null
+++ b/common/stream_iter.c
@@ -0,0 +1,104 @@
+/*
+ * Copyright (c) 2020, Alliance for Open Media. All rights reserved
+ *
+ * This source code is subject to the terms of the BSD 2 Clause License and
+ * the Alliance for Open Media Patent License 1.0. If the BSD 2 Clause License
+ * was not distributed with this source code in the LICENSE file, you can
+ * obtain it at www.aomedia.org/license/software. If the Alliance for Open
+ * Media Patent License 1.0 was not distributed with this source code in the
+ * PATENTS file, you can obtain it at www.aomedia.org/license/patent.
+ */
+
+#include <assert.h>
+#include <stdbool.h>
+#include <stdio.h>
+
+#include "common/stream_iter.h"
+#include "common/y4minput.h"
+
+static int copy_reader(StreamIter *iter, aom_image_t *img) {
+  struct AvxInputContext *input_ctx = iter->input.avx;
+  FILE *f = input_ctx->file;
+  y4m_input *y4m = &input_ctx->y4m;
+  int shortread = 0;
+
+  if (input_ctx->file_type == FILE_TYPE_Y4M) {
+    if (y4m_input_fetch_frame(y4m, f, img) < 1) return 0;
+  } else {
+    shortread = read_yuv_frame(input_ctx, img);
+  }
+  return !shortread;
+}
+
+void copy_stream_iter_init(StreamIter *iter, struct AvxInputContext *input) {
+  iter->input.avx = input;
+  iter->reader = copy_reader;
+}
+
+static int skip_reader(StreamIter *iter, aom_image_t *raw) {
+  // While we haven't skipped enough frames, read from the underlying
+  // stream and throw the result away. If no frame is available, early
+  // exit.
+  while (iter->current < iter->n) {
+    ++iter->current;
+    int frame_avail = read_stream_iter(iter->input.stream, raw);
+    if (!frame_avail) {
+      return frame_avail;
+    }
+  }
+  // If we're past the skip region, copy the remaining output.
+  return read_stream_iter(iter->input.stream, raw);
+}
+
+void skip_stream_iter_init(StreamIter *iter, StreamIter *input, int num_skip) {
+  assert(num_skip >= 0);
+  iter->input.stream = input;
+  iter->current = 0;
+  iter->n = num_skip;
+  iter->reader = skip_reader;
+}
+
+static int step_reader(StreamIter *iter, aom_image_t *raw) {
+  while (true) {
+    int frame_avail = read_stream_iter(iter->input.stream, raw);
+    // If at end of stream, no need to read further.
+    if (!frame_avail) {
+      return frame_avail;
+    }
+
+    bool should_encode = iter->current == 0;
+    iter->current = (iter->current + 1) % (iter->n);
+    if (should_encode) {
+      return frame_avail;
+    }
+  }
+}
+
+void step_stream_iter_init(StreamIter *iter, StreamIter *input, int step_size) {
+  assert(step_size > 0);
+  iter->input.stream = input;
+  iter->current = 0;
+  iter->n = step_size;
+  iter->reader = step_reader;
+}
+
+static int limit_reader(StreamIter *iter, aom_image_t *raw) {
+  // limit of 0 is a special case meaning "no limit".
+  if (iter->n != 0 && iter->current >= iter->n) {
+    return 0;
+  }
+  ++iter->current;
+  return read_stream_iter(iter->input.stream, raw);
+}
+
+void limit_stream_iter_init(StreamIter *iter, StreamIter *input, int limit) {
+  assert(limit >= 0);
+  iter->input.stream = input;
+  iter->current = 0;
+  iter->n = limit;
+  iter->reader = limit_reader;
+}
+
+int read_stream_iter(StreamIter *iter, aom_image_t *raw) {
+  return iter->reader(iter, raw);
+}
diff --git a/common/stream_iter.h b/common/stream_iter.h
new file mode 100644
index 0000000..af98a2a
--- /dev/null
+++ b/common/stream_iter.h
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2020, Alliance for Open Media. All rights reserved
+ *
+ * This source code is subject to the terms of the BSD 2 Clause License and
+ * the Alliance for Open Media Patent License 1.0. If the BSD 2 Clause License
+ * was not distributed with this source code in the LICENSE file, you can
+ * obtain it at www.aomedia.org/license/software. If the Alliance for Open
+ * Media Patent License 1.0 was not distributed with this source code in the
+ * PATENTS file, you can obtain it at www.aomedia.org/license/patent.
+ */
+#ifndef AOM_COMMON_STREAM_ITER_H_
+#define AOM_COMMON_STREAM_ITER_H_
+
+#include "aom/aom_image.h"
+#include "common/tools_common.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct StreamIter;
+
+// The input to each stream iterator can either be an AvxInputContext (when
+// reading from a file or a pipe like STDIN) or another stream iterator.
+union StreamInput {
+  struct AvxInputContext *avx;
+  struct StreamIter *stream;
+};
+
+typedef struct StreamIter {
+  union StreamInput input;
+  // For simplicity, all streams have two additional numbers associated
+  // with them, e.g., for counting the number of frames returned and the
+  // number of frames that should be skipped. Some iterators, like the copy
+  // iterator, do not use either field.
+  int current;
+  int n;
+  // Pointer to the function that performs the read. Returns whether a frame
+  // is available. If a frame is returned, it is written into *raw.
+  int (*reader)(struct StreamIter *iter, aom_image_t *raw);
+} StreamIter;
+
+// Iterator that simply copies the data from the AvxInputContext.
+void copy_stream_iter_init(StreamIter *iter, struct AvxInputContext *input);
+
+// Iterator that skips the first N frames. Takes another stream as input.
+void skip_stream_iter_init(StreamIter *iter, StreamIter *input, int num_skip);
+
+// Iterator that only returns every N-th frame. Takes another stream as input.
+void step_stream_iter_init(StreamIter *iter, StreamIter *input, int step_size);
+
+// Iterator that stops returning frames after the N-th. Takes another stream
+// as input.
+void limit_stream_iter_init(StreamIter *iter, StreamIter *input, int limit);
+
+// Invokes the iterator's specialized read function to read data from the
+// stream. Returns if a frame was read. If so, writes the data into *raw.
+int read_stream_iter(StreamIter *iter, aom_image_t *raw);
+
+#ifdef __cplusplus
+}  // extern "C"
+#endif
+
+#endif  // AOM_COMMON_STREAM_ITER_H_
diff --git a/test/stream_iter_test.cc b/test/stream_iter_test.cc
new file mode 100644
index 0000000..f7376ac
--- /dev/null
+++ b/test/stream_iter_test.cc
@@ -0,0 +1,285 @@
+/*
+ * Copyright (c) 2020, Alliance for Open Media. All rights reserved
+ *
+ * This source code is subject to the terms of the BSD 2 Clause License and
+ * the Alliance for Open Media Patent License 1.0. If the BSD 2 Clause License
+ * was not distributed with this source code in the LICENSE file, you can
+ * obtain it at www.aomedia.org/license/software. If the Alliance for Open
+ * Media Patent License 1.0 was not distributed with this source code in the
+ * PATENTS file, you can obtain it at www.aomedia.org/license/patent.
+ */
+
+#include <stdbool.h>
+#include "common/stream_iter.h"
+#include "third_party/googletest/src/googletest/include/gtest/gtest.h"
+
+namespace {
+
+// Dummy class that increments the width field in the image, which is used
+// as a placeholder for the frame number. Stops iterating after N frames.
+int dummy_reader(StreamIter *iter, aom_image_t *raw) {
+  if (iter->current >= iter->n) {
+    return 0;
+  }
+  ++iter->current;
+  raw->w = iter->current;
+  return 1;
+}
+
+void dummy_stream_iter_init(StreamIter *iter, int total) {
+  iter->current = 0;
+  iter->n = total;
+  iter->reader = dummy_reader;
+}
+
+class StreamIterTest : public ::testing::Test {};
+
+TEST_F(StreamIterTest, Skip0) {
+  StreamIter input;
+  dummy_stream_iter_init(&input, 3);
+
+  StreamIter skip;
+  skip_stream_iter_init(&skip, &input, 0);
+
+  aom_image_t img;
+  EXPECT_TRUE(read_stream_iter(&skip, &img));
+  EXPECT_EQ(1U, img.w);
+
+  EXPECT_TRUE(read_stream_iter(&skip, &img));
+  EXPECT_EQ(2U, img.w);
+
+  EXPECT_TRUE(read_stream_iter(&skip, &img));
+  EXPECT_EQ(3U, img.w);
+
+  EXPECT_FALSE(read_stream_iter(&skip, &img));
+}
+
+TEST_F(StreamIterTest, Skip1) {
+  StreamIter input;
+  dummy_stream_iter_init(&input, 3);
+
+  StreamIter skip;
+  skip_stream_iter_init(&skip, &input, 1);
+
+  aom_image_t img;
+  EXPECT_TRUE(read_stream_iter(&skip, &img));
+  EXPECT_EQ(2U, img.w);
+
+  EXPECT_TRUE(read_stream_iter(&skip, &img));
+  EXPECT_EQ(3U, img.w);
+
+  EXPECT_FALSE(read_stream_iter(&skip, &img));
+}
+
+TEST_F(StreamIterTest, Skip2) {
+  StreamIter input;
+  dummy_stream_iter_init(&input, 3);
+
+  StreamIter skip;
+  skip_stream_iter_init(&skip, &input, 2);
+
+  aom_image_t img;
+  EXPECT_TRUE(read_stream_iter(&skip, &img));
+  EXPECT_EQ(3U, img.w);
+
+  EXPECT_FALSE(read_stream_iter(&skip, &img));
+}
+
+TEST_F(StreamIterTest, Skip3) {
+  StreamIter input;
+  dummy_stream_iter_init(&input, 3);
+
+  StreamIter skip;
+  skip_stream_iter_init(&skip, &input, 3);
+
+  aom_image_t img;
+  EXPECT_FALSE(read_stream_iter(&skip, &img));
+}
+
+TEST_F(StreamIterTest, Skip4) {
+  StreamIter input;
+  dummy_stream_iter_init(&input, 3);
+
+  StreamIter skip;
+  skip_stream_iter_init(&skip, &input, 4);
+
+  aom_image_t img;
+  EXPECT_FALSE(read_stream_iter(&skip, &img));
+}
+
+// limit=0 is a special case meaning "no limit."
+TEST_F(StreamIterTest, Limit0) {
+  StreamIter input;
+  dummy_stream_iter_init(&input, 3);
+
+  StreamIter limit;
+  limit_stream_iter_init(&limit, &input, 0);
+
+  aom_image_t img;
+  EXPECT_TRUE(read_stream_iter(&limit, &img));
+  EXPECT_EQ(1U, img.w);
+
+  EXPECT_TRUE(read_stream_iter(&limit, &img));
+  EXPECT_EQ(2U, img.w);
+
+  EXPECT_TRUE(read_stream_iter(&limit, &img));
+  EXPECT_EQ(3U, img.w);
+
+  EXPECT_FALSE(read_stream_iter(&limit, &img));
+}
+
+TEST_F(StreamIterTest, Limit1) {
+  StreamIter input;
+  dummy_stream_iter_init(&input, 3);
+
+  StreamIter limit;
+  limit_stream_iter_init(&limit, &input, 1);
+
+  aom_image_t img;
+  EXPECT_TRUE(read_stream_iter(&limit, &img));
+  EXPECT_EQ(1U, img.w);
+
+  EXPECT_FALSE(read_stream_iter(&limit, &img));
+}
+
+TEST_F(StreamIterTest, Limit2) {
+  StreamIter input;
+  dummy_stream_iter_init(&input, 3);
+
+  StreamIter limit;
+  limit_stream_iter_init(&limit, &input, 2);
+
+  aom_image_t img;
+  EXPECT_TRUE(read_stream_iter(&limit, &img));
+  EXPECT_EQ(1U, img.w);
+
+  EXPECT_TRUE(read_stream_iter(&limit, &img));
+  EXPECT_EQ(2U, img.w);
+
+  EXPECT_FALSE(read_stream_iter(&limit, &img));
+}
+
+TEST_F(StreamIterTest, Limit3) {
+  StreamIter input;
+  dummy_stream_iter_init(&input, 3);
+
+  StreamIter limit;
+  limit_stream_iter_init(&limit, &input, 3);
+
+  aom_image_t img;
+  EXPECT_TRUE(read_stream_iter(&limit, &img));
+  EXPECT_EQ(1U, img.w);
+
+  EXPECT_TRUE(read_stream_iter(&limit, &img));
+  EXPECT_EQ(2U, img.w);
+
+  EXPECT_TRUE(read_stream_iter(&limit, &img));
+  EXPECT_EQ(3U, img.w);
+
+  EXPECT_FALSE(read_stream_iter(&limit, &img));
+}
+
+TEST_F(StreamIterTest, Limit4) {
+  StreamIter input;
+  dummy_stream_iter_init(&input, 3);
+
+  StreamIter limit;
+  limit_stream_iter_init(&limit, &input, 4);
+
+  aom_image_t img;
+  EXPECT_TRUE(read_stream_iter(&limit, &img));
+  EXPECT_EQ(1U, img.w);
+
+  EXPECT_TRUE(read_stream_iter(&limit, &img));
+  EXPECT_EQ(2U, img.w);
+
+  EXPECT_TRUE(read_stream_iter(&limit, &img));
+  EXPECT_EQ(3U, img.w);
+
+  EXPECT_FALSE(read_stream_iter(&limit, &img));
+}
+
+TEST_F(StreamIterTest, Step1) {
+  StreamIter input;
+  dummy_stream_iter_init(&input, 3);
+
+  StreamIter step;
+  step_stream_iter_init(&step, &input, 1);
+
+  aom_image_t img;
+  EXPECT_TRUE(read_stream_iter(&step, &img));
+  EXPECT_EQ(1U, img.w);
+
+  EXPECT_TRUE(read_stream_iter(&step, &img));
+  EXPECT_EQ(2U, img.w);
+
+  EXPECT_TRUE(read_stream_iter(&step, &img));
+  EXPECT_EQ(3U, img.w);
+
+  EXPECT_FALSE(read_stream_iter(&step, &img));
+}
+
+TEST_F(StreamIterTest, Step2) {
+  StreamIter input;
+  dummy_stream_iter_init(&input, 3);
+
+  StreamIter step;
+  step_stream_iter_init(&step, &input, 2);
+
+  aom_image_t img;
+  EXPECT_TRUE(read_stream_iter(&step, &img));
+  EXPECT_EQ(1U, img.w);
+
+  EXPECT_TRUE(read_stream_iter(&step, &img));
+  EXPECT_EQ(3U, img.w);
+
+  EXPECT_FALSE(read_stream_iter(&step, &img));
+}
+
+TEST_F(StreamIterTest, Step3) {
+  StreamIter input;
+  dummy_stream_iter_init(&input, 3);
+
+  StreamIter step;
+  step_stream_iter_init(&step, &input, 3);
+
+  aom_image_t img;
+  EXPECT_TRUE(read_stream_iter(&step, &img));
+  EXPECT_EQ(1U, img.w);
+
+  EXPECT_FALSE(read_stream_iter(&step, &img));
+}
+
+TEST_F(StreamIterTest, Step4) {
+  StreamIter input;
+  dummy_stream_iter_init(&input, 3);
+
+  StreamIter step;
+  step_stream_iter_init(&step, &input, 4);
+
+  aom_image_t img;
+  EXPECT_TRUE(read_stream_iter(&step, &img));
+  EXPECT_EQ(1U, img.w);
+
+  EXPECT_FALSE(read_stream_iter(&step, &img));
+}
+
+// Test when the step size is an even divisor of the number of frames.
+TEST_F(StreamIterTest, Step5) {
+  StreamIter input;
+  dummy_stream_iter_init(&input, 10);
+
+  StreamIter step;
+  step_stream_iter_init(&step, &input, 5);
+
+  aom_image_t img;
+  EXPECT_TRUE(read_stream_iter(&step, &img));
+  EXPECT_EQ(1U, img.w);
+
+  EXPECT_TRUE(read_stream_iter(&step, &img));
+  EXPECT_EQ(6U, img.w);
+
+  EXPECT_FALSE(read_stream_iter(&step, &img));
+}
+
+}  // namespace
diff --git a/test/test.cmake b/test/test.cmake
index 2ca7c64..8c6dfa3 100644
--- a/test/test.cmake
+++ b/test/test.cmake
@@ -218,6 +218,7 @@
               "${AOM_ROOT}/test/pickrst_test.cc"
               "${AOM_ROOT}/test/quantize_func_test.cc"
               "${AOM_ROOT}/test/sad_test.cc"
+              "${AOM_ROOT}/test/stream_iter_test.cc"
               "${AOM_ROOT}/test/subtract_test.cc"
               "${AOM_ROOT}/test/reconinter_test.cc"
               "${AOM_ROOT}/test/sum_squares_test.cc"