Add redundant MSDO support to decoder and multiplexer

Fixes issues #1312 and #1381
diff --git a/av2/decoder/decoder.h b/av2/decoder/decoder.h
index 2261020..47a9feb 100644
--- a/av2/decoder/decoder.h
+++ b/av2/decoder/decoder.h
@@ -319,6 +319,17 @@
   bool mfh_valid_buf[MAX_MFH_NUM];
 } StreamInfo;
 
+/*!
+ * \brief MSDO configuration snapshot for change detection
+ */
+typedef struct MsdoConfig {
+  BITSTREAM_PROFILE multistream_profile_idc;
+  AV2_LEVEL multistream_level_idx;
+  uint8_t multistream_tier_idx;
+  int num_streams;
+  int stream_ids[AVM_MAX_NUM_STREAMS];
+} MsdoConfig;
+
 typedef struct AV2Decoder {
   DecoderCodingBlock dcb;
 
diff --git a/av2/decoder/obu.c b/av2/decoder/obu.c
index 09ddf8b..db6b6c9 100644
--- a/av2/decoder/obu.c
+++ b/av2/decoder/obu.c
@@ -281,6 +281,11 @@
   int stream_idx = av2_get_stream_index(cm, xlayer_id);
   if (stream_idx < 0) return;  // Invalid or GLOBAL_XLAYER_ID
 
+  // Check if stream_info is valid
+  if (pbi->stream_info == NULL) {
+    return;
+  }
+
   for (int i = 0; i < REF_FRAMES; i++) {
     cm->ref_frame_map[i] = pbi->stream_info[stream_idx].ref_frame_map_buf[i];
     pbi->valid_for_referencing[i] =
@@ -368,6 +373,32 @@
   }
 }
 
+/*!
+ * \brief Save current MSDO configuration
+ */
+static void save_msdo_config(const AV2Decoder *pbi, MsdoConfig *config) {
+  const AV2_COMMON *const cm = &pbi->common;
+  config->multistream_profile_idc =
+      pbi->common.msdo_params.multistream_profile_idc;
+  config->multistream_tier_idx = pbi->common.msdo_params.multistream_tier_idx;
+  config->multistream_level_idx = pbi->common.msdo_params.multistream_level_idx;
+  config->num_streams = cm->num_streams;
+  memcpy(config->stream_ids, cm->stream_ids, cm->num_streams * sizeof(int));
+}
+
+/*!
+ * \brief Compare two MSDO configurations
+ * \return true if configurations are identical, false otherwise
+ */
+static bool msdo_config_equal(const MsdoConfig *a, const MsdoConfig *b) {
+  if (a->multistream_profile_idc != b->multistream_profile_idc) return false;
+  if (a->multistream_tier_idx != b->multistream_tier_idx) return false;
+  if (a->multistream_level_idx != b->multistream_level_idx) return false;
+  if (a->num_streams != b->num_streams) return false;
+
+  return true;
+}
+
 static uint32_t read_multi_stream_decoder_operation_obu(
     AV2Decoder *pbi, struct avm_read_bit_buffer *rb) {
   AV2_COMMON *const cm = &pbi->common;
@@ -375,6 +406,16 @@
 
   // Verify rb has been configured to report errors.
   assert(rb->error_handler);
+
+  // Save previous configuration if it exists
+  MsdoConfig prev_config;
+  bool has_previous = false;
+
+  if (pbi->stream_info != NULL) {
+    save_msdo_config(pbi, &prev_config);
+    has_previous = true;
+  }
+
   const int num_streams =
       avm_rb_read_literal(rb, 3) + 2;  // read number of streams
   if (num_streams > AVM_MAX_NUM_STREAMS) {
@@ -416,19 +457,32 @@
         avm_rb_read_bit(rb);  // read tier of multistream
     (void)substream_tier_idx;
   }
-  // Allocate intermediate buffers to store internal variables per sub-stream
-  if (pbi->stream_info != NULL) {
+
+  // Check if configuration changed
+  MsdoConfig new_config;
+  save_msdo_config(pbi, &new_config);
+  bool config_changed =
+      !has_previous || !msdo_config_equal(&prev_config, &new_config);
+
+  // Only free if stream_info exists AND config changed
+  if (pbi->stream_info != NULL && config_changed) {
     avm_free(pbi->stream_info);
     pbi->stream_info = NULL;
   }
-  pbi->stream_info = (StreamInfo *)avm_malloc(num_streams * sizeof(StreamInfo));
+
+  // Only allocate if stream_info is NULL (first time OR after freeing due to
+  // config change)
   if (pbi->stream_info == NULL) {
-    avm_internal_error(&cm->error, AVM_CODEC_MEM_ERROR,
-                       "Memory allocation failed for pbi->stream_info\n");
-  }
-  memset(pbi->stream_info, 0, num_streams * sizeof(StreamInfo));
-  for (int i = 0; i < num_streams; i++) {
-    init_stream_info(&pbi->stream_info[i]);
+    pbi->stream_info =
+        (StreamInfo *)avm_malloc(num_streams * sizeof(StreamInfo));
+    if (pbi->stream_info == NULL) {
+      avm_internal_error(&cm->error, AVM_CODEC_MEM_ERROR,
+                         "Memory allocation failed for pbi->stream_info\n");
+    }
+    memset(pbi->stream_info, 0, num_streams * sizeof(StreamInfo));
+    for (int i = 0; i < num_streams; i++) {
+      init_stream_info(&pbi->stream_info[i]);
+    }
   }
 
   pbi->msdo_is_present_in_tu = 1;
@@ -2957,40 +3011,38 @@
 
     cm->tlayer_id = obu_header.obu_tlayer_id;
     cm->mlayer_id = obu_header.obu_mlayer_id;
-    if (obu_header.type == OBU_MSDO) {
+
+    if (!pbi->multi_stream_mode ||
+        (obu_header.obu_xlayer_id == GLOBAL_XLAYER_ID &&
+         cm->xlayer_id == GLOBAL_XLAYER_ID)) {
       cm->xlayer_id = obu_header.obu_xlayer_id;
-    } else {
-      if (!pbi->multi_stream_mode ||
-          (obu_header.obu_xlayer_id == GLOBAL_XLAYER_ID &&
-           cm->xlayer_id == GLOBAL_XLAYER_ID)) {
-        cm->xlayer_id = obu_header.obu_xlayer_id;
-      } else if (cm->xlayer_id != GLOBAL_XLAYER_ID &&
-                 obu_header.obu_xlayer_id == GLOBAL_XLAYER_ID) {
-        // Store xlayer context
-        av2_store_xlayer_context(pbi, cm, cm->xlayer_id);
-        cm->xlayer_id = obu_header.obu_xlayer_id;
-      } else if (cm->xlayer_id == GLOBAL_XLAYER_ID &&
-                 obu_header.obu_xlayer_id != GLOBAL_XLAYER_ID) {
-        // Restore xlayer context
-        cm->xlayer_id = obu_header.obu_xlayer_id;
-        av2_restore_xlayer_context(pbi, cm, cm->xlayer_id);
-      } else if (cm->xlayer_id != obu_header.obu_xlayer_id) {
-        // Store and restore xlayer context
-        av2_store_xlayer_context(pbi, cm, cm->xlayer_id);
-        cm->xlayer_id = obu_header.obu_xlayer_id;
-        av2_restore_xlayer_context(pbi, cm, cm->xlayer_id);
-      }
-      if (obu_header.type == OBU_LEADING_TILE_GROUP ||
-          obu_header.type == OBU_REGULAR_TILE_GROUP) {
-        if (prev_obu_xlayer_id == -1) {
-          prev_obu_xlayer_id = obu_header.obu_xlayer_id;
-        } else {
-          if (pbi->multi_stream_mode && prev_obu_xlayer_id >= 0 &&
-              obu_header.obu_xlayer_id != prev_obu_xlayer_id) {
-            avm_internal_error(&cm->error, AVM_CODEC_UNSUP_BITSTREAM,
-                               "tile group OBUs with the same stream_id shall "
-                               "be contiguous within a temporal unit");
-          }
+    } else if (cm->xlayer_id != GLOBAL_XLAYER_ID &&
+               obu_header.obu_xlayer_id == GLOBAL_XLAYER_ID) {
+      // Store xlayer context
+      av2_store_xlayer_context(pbi, cm, cm->xlayer_id);
+      cm->xlayer_id = obu_header.obu_xlayer_id;
+    } else if (cm->xlayer_id == GLOBAL_XLAYER_ID &&
+               obu_header.obu_xlayer_id != GLOBAL_XLAYER_ID) {
+      // Restore xlayer context
+      cm->xlayer_id = obu_header.obu_xlayer_id;
+      av2_restore_xlayer_context(pbi, cm, cm->xlayer_id);
+    } else if (cm->xlayer_id != obu_header.obu_xlayer_id) {
+      // Store and restore xlayer context
+      av2_store_xlayer_context(pbi, cm, cm->xlayer_id);
+      cm->xlayer_id = obu_header.obu_xlayer_id;
+      av2_restore_xlayer_context(pbi, cm, cm->xlayer_id);
+    }
+
+    if (obu_header.type == OBU_LEADING_TILE_GROUP ||
+        obu_header.type == OBU_REGULAR_TILE_GROUP) {
+      if (prev_obu_xlayer_id == -1) {
+        prev_obu_xlayer_id = obu_header.obu_xlayer_id;
+      } else {
+        if (pbi->multi_stream_mode && prev_obu_xlayer_id >= 0 &&
+            obu_header.obu_xlayer_id != prev_obu_xlayer_id) {
+          avm_internal_error(&cm->error, AVM_CODEC_UNSUP_BITSTREAM,
+                             "tile group OBUs with the same stream_id shall "
+                             "be contiguous within a temporal unit");
         }
       }
     }
diff --git a/tools/stream_multiplexer.cc b/tools/stream_multiplexer.cc
index 0a43f64..122568c 100644
--- a/tools/stream_multiplexer.cc
+++ b/tools/stream_multiplexer.cc
@@ -96,7 +96,7 @@
 std::vector<uint8_t> WriteTU(const uint8_t *data, int length,
                              int *obu_overhead_bytes, int seg_idx,
                              int num_streams, int *stream_ids,
-                             int *stream_buffer_units) {
+                             int *stream_buffer_units, bool redundant_msdo) {
   std::vector<uint8_t> tu_obus;
   const uint8_t *data_ptr = data;
   const int kObuHeaderSizeBytes = 1;
@@ -105,6 +105,7 @@
   int obu_overhead = 0;
   int msdo_signaled = 0;
   ObuHeader obu_header;
+
   while (consumed < length) {
     const int remaining = length - consumed;
     if (remaining < kMinimumBytesRequired) {
@@ -162,15 +163,16 @@
                                  data_ptr + obu_total_size + length_field_size);
 
     if (!msdo_signaled &&
-        (obu_header.type == OBU_LAYER_CONFIGURATION_RECORD ||
-         obu_header.type == OBU_OPERATING_POINT_SET ||
-         obu_header.type == OBU_ATLAS_SEGMENT ||
-         ((obu_header.type == OBU_METADATA_SHORT ||
-           obu_header.type == OBU_METADATA_GROUP) &&
-          obu_header.obu_header_extension_flag &&
-          obu_header.obu_xlayer_id == GLOBAL_XLAYER_ID) ||
-         obu_header.type == OBU_SEQUENCE_HEADER) &&
-        (seg_idx == 0)) {
+        (redundant_msdo ||
+         ((obu_header.type == OBU_LAYER_CONFIGURATION_RECORD ||
+           obu_header.type == OBU_OPERATING_POINT_SET ||
+           obu_header.type == OBU_ATLAS_SEGMENT ||
+           ((obu_header.type == OBU_METADATA_SHORT ||
+             obu_header.type == OBU_METADATA_GROUP) &&
+            obu_header.obu_header_extension_flag &&
+            obu_header.obu_xlayer_id == GLOBAL_XLAYER_ID) ||
+           obu_header.type == OBU_SEQUENCE_HEADER) &&
+          seg_idx == 0))) {
       std::vector<uint8_t> multi_stream_obu(num_streams * 2 + 4);
       int multi_header_obu_size = write_multi_stream_decoder_operation_obu(
           multi_stream_obu.data(), num_streams, stream_ids,
@@ -226,24 +228,34 @@
 }
 
 int main(int argc, const char *argv[]) {
-  if (argc < 3 || (argc - 2) % 3) {
+  bool redundant_msdo = false;
+  int arg_offset = 0;
+
+  // Check for --redundant-msdo flag
+  if (argc > 1 && strcmp(argv[1], "--redundant-msdo") == 0) {
+    redundant_msdo = true;
+    arg_offset = 1;
+  }
+
+  if (argc - arg_offset < 3 || (argc - arg_offset - 2) % 3) {
     fprintf(stderr,
-            "command: %s [input file1], [stream ID 1], [unit size 1], [input "
+            "command: %s [--redundant-msdo] [input file1], [stream ID 1], "
+            "[unit size 1], [input "
             "file2], [stream ID 2], [unit size 2], ... [outfile]\n",
             argv[0]);
     return -1;
-  } else if (argc > (AVM_MAX_NUM_STREAMS * 3 + 2)) {
+  } else if (argc - arg_offset > (AVM_MAX_NUM_STREAMS * 3 + 2)) {
     fprintf(stderr,
             "The number of input files cannot exceed the maximum number of "
             "streams (8)\n");
     return -1;
   }
 
-  int num_streams = (argc - 2) / 3;
+  int num_streams = (argc - arg_offset - 2) / 3;
   int sum_buffer_units = 0;
 
   for (int i = 0; i < num_streams; ++i) {
-    int stream_id = atoi(argv[i * 3 + 2]);
+    int stream_id = atoi(argv[arg_offset + i * 3 + 2]);
     if (stream_id > 7) {
       fprintf(stderr,
               "The value of stream_id cannot exceed the max value (7)\n");
@@ -251,7 +263,7 @@
     }
   }
   for (int i = 0; i < num_streams; ++i) {
-    sum_buffer_units += atoi(argv[i * 3 + 3]);
+    sum_buffer_units += atoi(argv[arg_offset + i * 3 + 3]);
   }
   if (sum_buffer_units > 8) {
     fprintf(stderr,
@@ -278,7 +290,7 @@
 
   // Initialize file read for each stream
   for (int i = 0; i < num_streams; ++i) {
-    fin[i] = fopen(argv[i * 3 + 1], "rb");
+    fin[i] = fopen(argv[arg_offset + i * 3 + 1], "rb");
     if (fin[i] == nullptr) {
       fprintf(stderr, "Error: failed to open the input file\n");
     }
@@ -313,7 +325,7 @@
   printf("  Stream IDs: ");
 #endif  // PRINT_TU_INFO
   for (int i = 0; i < num_streams; ++i) {
-    stream_ids[i] = atoi(argv[i * 3 + 2]);
+    stream_ids[i] = atoi(argv[arg_offset + i * 3 + 2]);
 #if PRINT_TU_INFO
     printf("[%d] ", stream_ids[i]);
 #endif  // PRINT_TU_INFO
@@ -325,7 +337,7 @@
   // Set the values of unit sizes of streams
   int stream_buffer_units[AVM_MAX_NUM_STREAMS];
   for (int i = 0; i < num_streams; ++i) {
-    stream_buffer_units[i] = atoi(argv[i * 3 + 3]);
+    stream_buffer_units[i] = atoi(argv[arg_offset + i * 3 + 3]);
 #if PRINT_TU_INFO
     printf("[%d] ", stream_buffer_units[i]);
 #endif  // PRINT_TU_INFO
@@ -353,7 +365,7 @@
         segments =
             WriteTU(input_ctx[i].unit_buffer, static_cast<int>(unit_size),
                     &obu_overhead_current_unit, i, num_streams, stream_ids,
-                    stream_buffer_units);
+                    stream_buffer_units, redundant_msdo);
         fwrite(segments.data(), 1, segments.size(), fout);
 #if PRINT_TU_INFO
         printf("  TU overhead:    %d\n", obu_overhead_current_unit);