blob: caaa7869ee2cad1e509f805c4558ca0af3360ccc [file] [log] [blame]
/*
* 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.text.webvtt;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.SimpleSubtitleDecoder;
import com.google.android.exoplayer2.text.Subtitle;
import com.google.android.exoplayer2.text.SubtitleDecoderException;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/** A {@link SimpleSubtitleDecoder} for Webvtt embedded in a Mp4 container file. */
@SuppressWarnings("ConstantField")
public final class Mp4WebvttDecoder extends SimpleSubtitleDecoder {
private static final int BOX_HEADER_SIZE = 8;
@SuppressWarnings("ConstantCaseForConstants")
private static final int TYPE_payl = 0x7061796c;
@SuppressWarnings("ConstantCaseForConstants")
private static final int TYPE_sttg = 0x73747467;
@SuppressWarnings("ConstantCaseForConstants")
private static final int TYPE_vttc = 0x76747463;
private final ParsableByteArray sampleData;
public Mp4WebvttDecoder() {
super("Mp4WebvttDecoder");
sampleData = new ParsableByteArray();
}
@Override
protected Subtitle decode(byte[] bytes, int length, boolean reset)
throws SubtitleDecoderException {
// Webvtt in Mp4 samples have boxes inside of them, so we have to do a traditional box parsing:
// first 4 bytes size and then 4 bytes type.
sampleData.reset(bytes, length);
List<Cue> resultingCueList = new ArrayList<>();
while (sampleData.bytesLeft() > 0) {
if (sampleData.bytesLeft() < BOX_HEADER_SIZE) {
throw new SubtitleDecoderException("Incomplete Mp4Webvtt Top Level box header found.");
}
int boxSize = sampleData.readInt();
int boxType = sampleData.readInt();
if (boxType == TYPE_vttc) {
resultingCueList.add(parseVttCueBox(sampleData, boxSize - BOX_HEADER_SIZE));
} else {
// Peers of the VTTCueBox are still not supported and are skipped.
sampleData.skipBytes(boxSize - BOX_HEADER_SIZE);
}
}
return new Mp4WebvttSubtitle(resultingCueList);
}
private static Cue parseVttCueBox(ParsableByteArray sampleData, int remainingCueBoxBytes)
throws SubtitleDecoderException {
@Nullable Cue.Builder cueBuilder = null;
@Nullable CharSequence cueText = null;
while (remainingCueBoxBytes > 0) {
if (remainingCueBoxBytes < BOX_HEADER_SIZE) {
throw new SubtitleDecoderException("Incomplete vtt cue box header found.");
}
int boxSize = sampleData.readInt();
int boxType = sampleData.readInt();
remainingCueBoxBytes -= BOX_HEADER_SIZE;
int payloadLength = boxSize - BOX_HEADER_SIZE;
String boxPayload =
Util.fromUtf8Bytes(sampleData.getData(), sampleData.getPosition(), payloadLength);
sampleData.skipBytes(payloadLength);
remainingCueBoxBytes -= payloadLength;
if (boxType == TYPE_sttg) {
cueBuilder = WebvttCueParser.parseCueSettingsList(boxPayload);
} else if (boxType == TYPE_payl) {
cueText =
WebvttCueParser.parseCueText(
/* id= */ null, boxPayload.trim(), /* styles= */ Collections.emptyList());
} else {
// Other VTTCueBox children are still not supported and are ignored.
}
}
if (cueText == null) {
cueText = "";
}
return cueBuilder != null
? cueBuilder.setText(cueText).build()
: WebvttCueParser.newCueForText(cueText);
}
}