Back to Repositories

Testing SCTE-35 Splice Info Decoder Implementation in SmartTube

This test suite validates the SpliceInfoDecoder functionality in ExoPlayer, focusing on SCTE-35 signal processing and timestamp handling. The tests ensure accurate decoding of splice commands and proper handling of timing information for media stream manipulation.

Test Coverage Overview

The test suite provides comprehensive coverage of SCTE-35 signal decoding functionality.

Key areas tested include:
  • Time signal command processing with wrapped-around timestamps
  • Multiple splice insert command handling
  • Component splice verification
  • PTS adjustment and timing accuracy

Implementation Analysis

The testing approach uses JUnit with AndroidJUnit4 runner for Android-specific testing context. The implementation follows a systematic pattern of setting up test fixtures, feeding input buffers with SCTE-35 data, and verifying decoded metadata output.

Key patterns include byte-level command construction and timestamp manipulation using the TimestampAdjuster utility.

Technical Details

Testing tools and configuration:
  • JUnit test framework with AndroidJUnit4 runner
  • MetadataInputBuffer for raw data handling
  • ByteBuffer for data manipulation
  • Custom TimestampAdjuster for PTS conversion
  • Truth assertion library for test verification

Best Practices Demonstrated

The test suite exemplifies several testing best practices including proper test isolation through @Before setup, comprehensive edge case coverage, and detailed verification of complex data structures. The code organization follows clear separation of concerns with distinct test methods for different command types and scenarios.

yuliskov/smarttube

exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoderTest.java

            
/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.android.exoplayer2.metadata.scte35;

import static com.google.android.exoplayer2.C.TIME_UNSET;
import static com.google.common.truth.Truth.assertThat;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
import com.google.android.exoplayer2.util.TimestampAdjuster;
import java.nio.ByteBuffer;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

/** Test for {@link SpliceInfoDecoder}. */
@RunWith(AndroidJUnit4.class)
public final class SpliceInfoDecoderTest {

  private SpliceInfoDecoder decoder;
  private MetadataInputBuffer inputBuffer;

  @Before
  public void setUp() {
    decoder = new SpliceInfoDecoder();
    inputBuffer = new MetadataInputBuffer();
  }

  @Test
  public void testWrappedAroundTimeSignalCommand() {
    byte[] rawTimeSignalSection = new byte[] {
        0, // table_id.
        (byte) 0x80, // section_syntax_indicator, private_indicator, reserved, section_length(4).
        0x14, // section_length(8).
        0x00, // protocol_version.
        0x00, // encrypted_packet, encryption_algorithm, pts_adjustment(1).
        0x00, 0x00, 0x00, 0x00, // pts_adjustment(32).
        0x00, // cw_index.
        0x00, // tier(8).
        0x00, // tier(4), splice_command_length(4).
        0x05, // splice_command_length(8).
        0x06, // splice_command_type = time_signal.
        // Start of splice_time().
        (byte) 0x80, // time_specified_flag, reserved, pts_time(1).
        0x52, 0x03, 0x02, (byte) 0x8f, // pts_time(32). PTS for a second after playback position.
        0x00, 0x00, 0x00, 0x00}; // CRC_32 (ignored, check happens at extraction).

    // The playback position is 57:15:58.43 approximately.
    // With this offset, the playback position pts before wrapping is 0x451ebf851.
    Metadata metadata = feedInputBuffer(rawTimeSignalSection, 0x3000000000L, -0x50000L);
    assertThat(metadata.length()).isEqualTo(1);
    assertThat(((TimeSignalCommand) metadata.get(0)).playbackPositionUs)
        .isEqualTo(removePtsConversionPrecisionError(0x3001000000L, inputBuffer.subsampleOffsetUs));
  }

  @Test
  public void test2SpliceInsertCommands() {
    byte[] rawSpliceInsertCommand1 = new byte[] {
        0, // table_id.
        (byte) 0x80, // section_syntax_indicator, private_indicator, reserved, section_length(4).
        0x19, // section_length(8).
        0x00, // protocol_version.
        0x00, // encrypted_packet, encryption_algorithm, pts_adjustment(1).
        0x00, 0x00, 0x00, 0x00, // pts_adjustment(32).
        0x00, // cw_index.
        0x00, // tier(8).
        0x00, // tier(4), splice_command_length(4).
        0x0e, // splice_command_length(8).
        0x05, // splice_command_type = splice_insert.
        // Start of splice_insert().
        0x00, 0x00, 0x00, 0x42, // splice_event_id.
        0x00, // splice_event_cancel_indicator, reserved.
        0x40, // out_of_network_indicator, program_splice_flag, duration_flag,
              // splice_immediate_flag, reserved.
        // start of splice_time().
        (byte) 0x80, // time_specified_flag, reserved, pts_time(1).
        0x00, 0x00, 0x00, 0x00, // PTS for playback position 3s.
        0x00, 0x10, // unique_program_id.
        0x01, // avail_num.
        0x02, // avails_expected.
        0x00, 0x00, 0x00, 0x00}; // CRC_32 (ignored, check happens at extraction).

    Metadata metadata = feedInputBuffer(rawSpliceInsertCommand1, 2000000, 3000000);
    assertThat(metadata.length()).isEqualTo(1);
    SpliceInsertCommand command = (SpliceInsertCommand) metadata.get(0);
    assertThat(command.spliceEventId).isEqualTo(66);
    assertThat(command.spliceEventCancelIndicator).isFalse();
    assertThat(command.outOfNetworkIndicator).isFalse();
    assertThat(command.programSpliceFlag).isTrue();
    assertThat(command.spliceImmediateFlag).isFalse();
    assertThat(command.programSplicePlaybackPositionUs).isEqualTo(3000000);
    assertThat(command.breakDurationUs).isEqualTo(TIME_UNSET);
    assertThat(command.uniqueProgramId).isEqualTo(16);
    assertThat(command.availNum).isEqualTo(1);
    assertThat(command.availsExpected).isEqualTo(2);

    byte[] rawSpliceInsertCommand2 = new byte[] {
        0, // table_id.
        (byte) 0x80, // section_syntax_indicator, private_indicator, reserved, section_length(4).
        0x22, // section_length(8).
        0x00, // protocol_version.
        0x00, // encrypted_packet, encryption_algorithm, pts_adjustment(1).
        0x00, 0x00, 0x00, 0x00, // pts_adjustment(32).
        0x00, // cw_index.
        0x00, // tier(8).
        0x00, // tier(4), splice_command_length(4).
        0x13, // splice_command_length(8).
        0x05, // splice_command_type = splice_insert.
        // Start of splice_insert().
        (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // splice_event_id.
        0x00, // splice_event_cancel_indicator, reserved.
        0x00, // out_of_network_indicator, program_splice_flag, duration_flag,
              // splice_immediate_flag, reserved.
        0x02, // component_count.
        0x10, // component_tag.
        // start of splice_time().
        (byte) 0x81, // time_specified_flag, reserved, pts_time(1).
        (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // PTS for playback position 10s.
        // start of splice_time().
        0x11, // component_tag.
        0x00, // time_specified_flag, reserved.
        0x00, 0x20, // unique_program_id.
        0x01, // avail_num.
        0x02, // avails_expected.
        0x00, 0x00, 0x00, 0x00}; // CRC_32 (ignored, check happens at extraction).

    // By changing the subsample offset we force adjuster reconstruction.
    long subsampleOffset = 1000011;
    metadata = feedInputBuffer(rawSpliceInsertCommand2, 1000000, subsampleOffset);
    assertThat(metadata.length()).isEqualTo(1);
    command = (SpliceInsertCommand) metadata.get(0);
    assertThat(command.spliceEventId).isEqualTo(0xffffffffL);
    assertThat(command.spliceEventCancelIndicator).isFalse();
    assertThat(command.outOfNetworkIndicator).isFalse();
    assertThat(command.programSpliceFlag).isFalse();
    assertThat(command.spliceImmediateFlag).isFalse();
    assertThat(command.programSplicePlaybackPositionUs).isEqualTo(TIME_UNSET);
    assertThat(command.breakDurationUs).isEqualTo(TIME_UNSET);
    List<SpliceInsertCommand.ComponentSplice> componentSplices = command.componentSpliceList;
    assertThat(componentSplices).hasSize(2);
    assertThat(componentSplices.get(0).componentTag).isEqualTo(16);
    assertThat(componentSplices.get(0).componentSplicePlaybackPositionUs).isEqualTo(1000000);
    assertThat(componentSplices.get(1).componentTag).isEqualTo(17);
    assertThat(componentSplices.get(1).componentSplicePts).isEqualTo(TIME_UNSET);
    assertThat(command.uniqueProgramId).isEqualTo(32);
    assertThat(command.availNum).isEqualTo(1);
    assertThat(command.availsExpected).isEqualTo(2);
  }

  private Metadata feedInputBuffer(byte[] data, long timeUs, long subsampleOffset) {
    inputBuffer.clear();
    inputBuffer.data = ByteBuffer.allocate(data.length).put(data);
    inputBuffer.timeUs = timeUs;
    inputBuffer.subsampleOffsetUs = subsampleOffset;
    return decoder.decode(inputBuffer);
  }

  private static long removePtsConversionPrecisionError(long timeUs, long offsetUs) {
    return TimestampAdjuster.ptsToUs(TimestampAdjuster.usToPts(timeUs - offsetUs)) + offsetUs;
  }

}