summaryrefslogtreecommitdiffstats
path: root/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsBinarySearchSeeker.java
blob: acd08a2f12795be5d51db1e65aeaaea2e6d4e7f1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
/*
 * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts;

import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.BinarySearchSeeker;
import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster;
import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
import java.io.IOException;

/**
 * A seeker that supports seeking within PS stream using binary search.
 *
 * <p>This seeker uses the first and last SCR values within the stream, as well as the stream
 * duration to interpolate the SCR value of the seeking position. Then it performs binary search
 * within the stream to find a packets whose SCR value is with in {@link #SEEK_TOLERANCE_US} from
 * the target SCR.
 */
/* package */ final class PsBinarySearchSeeker extends BinarySearchSeeker {

  private static final long SEEK_TOLERANCE_US = 100_000;
  private static final int MINIMUM_SEARCH_RANGE_BYTES = 1000;
  private static final int TIMESTAMP_SEARCH_BYTES = 20000;

  public PsBinarySearchSeeker(
      TimestampAdjuster scrTimestampAdjuster, long streamDurationUs, long inputLength) {
    super(
        new DefaultSeekTimestampConverter(),
        new PsScrSeeker(scrTimestampAdjuster),
        streamDurationUs,
        /* floorTimePosition= */ 0,
        /* ceilingTimePosition= */ streamDurationUs + 1,
        /* floorBytePosition= */ 0,
        /* ceilingBytePosition= */ inputLength,
        /* approxBytesPerFrame= */ TsExtractor.TS_PACKET_SIZE,
        MINIMUM_SEARCH_RANGE_BYTES);
  }

  /**
   * A seeker that looks for a given SCR timestamp at a given position in a PS stream.
   *
   * <p>Given a SCR timestamp, and a position within a PS stream, this seeker will peek up to {@link
   * #TIMESTAMP_SEARCH_BYTES} bytes from that stream position, look for all packs in that range, and
   * then compare the SCR timestamps (if available) of these packets to the target timestamp.
   */
  private static final class PsScrSeeker implements TimestampSeeker {

    private final TimestampAdjuster scrTimestampAdjuster;
    private final ParsableByteArray packetBuffer;

    private PsScrSeeker(TimestampAdjuster scrTimestampAdjuster) {
      this.scrTimestampAdjuster = scrTimestampAdjuster;
      packetBuffer = new ParsableByteArray();
    }

    @Override
    public TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetTimestamp)
        throws IOException, InterruptedException {
      long inputPosition = input.getPosition();
      int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, input.getLength() - inputPosition);

      packetBuffer.reset(bytesToSearch);
      input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToSearch);

      return searchForScrValueInBuffer(packetBuffer, targetTimestamp, inputPosition);
    }

    @Override
    public void onSeekFinished() {
      packetBuffer.reset(Util.EMPTY_BYTE_ARRAY);
    }

    private TimestampSearchResult searchForScrValueInBuffer(
        ParsableByteArray packetBuffer, long targetScrTimeUs, long bufferStartOffset) {
      int startOfLastPacketPosition = C.POSITION_UNSET;
      int endOfLastPacketPosition = C.POSITION_UNSET;
      long lastScrTimeUsInRange = C.TIME_UNSET;

      while (packetBuffer.bytesLeft() >= 4) {
        int nextStartCode = peekIntAtPosition(packetBuffer.data, packetBuffer.getPosition());
        if (nextStartCode != PsExtractor.PACK_START_CODE) {
          packetBuffer.skipBytes(1);
          continue;
        } else {
          packetBuffer.skipBytes(4);
        }

        // We found a pack.
        long scrValue = PsDurationReader.readScrValueFromPack(packetBuffer);
        if (scrValue != C.TIME_UNSET) {
          long scrTimeUs = scrTimestampAdjuster.adjustTsTimestamp(scrValue);
          if (scrTimeUs > targetScrTimeUs) {
            if (lastScrTimeUsInRange == C.TIME_UNSET) {
              // First SCR timestamp is already over target.
              return TimestampSearchResult.overestimatedResult(scrTimeUs, bufferStartOffset);
            } else {
              // Last SCR timestamp < target timestamp < this timestamp.
              return TimestampSearchResult.targetFoundResult(
                  bufferStartOffset + startOfLastPacketPosition);
            }
          } else if (scrTimeUs + SEEK_TOLERANCE_US > targetScrTimeUs) {
            long startOfPacketInStream = bufferStartOffset + packetBuffer.getPosition();
            return TimestampSearchResult.targetFoundResult(startOfPacketInStream);
          }

          lastScrTimeUsInRange = scrTimeUs;
          startOfLastPacketPosition = packetBuffer.getPosition();
        }
        skipToEndOfCurrentPack(packetBuffer);
        endOfLastPacketPosition = packetBuffer.getPosition();
      }

      if (lastScrTimeUsInRange != C.TIME_UNSET) {
        long endOfLastPacketPositionInStream = bufferStartOffset + endOfLastPacketPosition;
        return TimestampSearchResult.underestimatedResult(
            lastScrTimeUsInRange, endOfLastPacketPositionInStream);
      } else {
        return TimestampSearchResult.NO_TIMESTAMP_IN_RANGE_RESULT;
      }
    }

    /**
     * Skips the buffer position to the position after the end of the current PS pack in the buffer,
     * given the byte position right after the {@link PsExtractor#PACK_START_CODE} of the pack in
     * the buffer. If the pack ends after the end of the buffer, skips to the end of the buffer.
     */
    private static void skipToEndOfCurrentPack(ParsableByteArray packetBuffer) {
      int limit = packetBuffer.limit();

      if (packetBuffer.bytesLeft() < 10) {
        // We require at least 9 bytes for pack header to read SCR value + 1 byte for pack_stuffing
        // length.
        packetBuffer.setPosition(limit);
        return;
      }
      packetBuffer.skipBytes(9);

      int packStuffingLength = packetBuffer.readUnsignedByte() & 0x07;
      if (packetBuffer.bytesLeft() < packStuffingLength) {
        packetBuffer.setPosition(limit);
        return;
      }
      packetBuffer.skipBytes(packStuffingLength);

      if (packetBuffer.bytesLeft() < 4) {
        packetBuffer.setPosition(limit);
        return;
      }

      int nextStartCode = peekIntAtPosition(packetBuffer.data, packetBuffer.getPosition());
      if (nextStartCode == PsExtractor.SYSTEM_HEADER_START_CODE) {
        packetBuffer.skipBytes(4);
        int systemHeaderLength = packetBuffer.readUnsignedShort();
        if (packetBuffer.bytesLeft() < systemHeaderLength) {
          packetBuffer.setPosition(limit);
          return;
        }
        packetBuffer.skipBytes(systemHeaderLength);
      }

      // Find the position of the next PACK_START_CODE or MPEG_PROGRAM_END_CODE, which is right
      // after the end position of this pack.
      // If we couldn't find these codes within the buffer, return the buffer limit, or return
      // the first position which PES packets pattern does not match (some malformed packets).
      while (packetBuffer.bytesLeft() >= 4) {
        nextStartCode = peekIntAtPosition(packetBuffer.data, packetBuffer.getPosition());
        if (nextStartCode == PsExtractor.PACK_START_CODE
            || nextStartCode == PsExtractor.MPEG_PROGRAM_END_CODE) {
          break;
        }
        if (nextStartCode >>> 8 != PsExtractor.PACKET_START_CODE_PREFIX) {
          break;
        }
        packetBuffer.skipBytes(4);

        if (packetBuffer.bytesLeft() < 2) {
          // 2 bytes for PES_packet length.
          packetBuffer.setPosition(limit);
          return;
        }
        int pesPacketLength = packetBuffer.readUnsignedShort();
        packetBuffer.setPosition(
            Math.min(packetBuffer.limit(), packetBuffer.getPosition() + pesPacketLength));
      }
    }
  }

  private static int peekIntAtPosition(byte[] data, int position) {
    return (data[position] & 0xFF) << 24
        | (data[position + 1] & 0xFF) << 16
        | (data[position + 2] & 0xFF) << 8
        | (data[position + 3] & 0xFF);
  }
}